Skip to content

Profile-guided optimisation (PGO) for the Switch NRO #37

Open

Description

@Pulpparty

Investigate whether profile-guided optimisation (PGO) is worth adding to the Switch NRO, and how. Not scheduled.

Context: SwitchWakerHD added optional PGO for its Switch build (centollOS/SwitchWakerHD#87, docs/pgo.md): +2.6% to +7.5% fps at stock clocks, main thread -4.7% to -6.1%. It stays off there because the profile depends on each player's own recompile (centollOS/SwitchWakerHD#90). That problem does not exist here: the NRO is built by the maintainers from the decomp sources, so a profile is just a build input made once per release.

What is different from HD:

  • Two compilers. The game units are compiled with clang 19 (switch/native/clang-launcher.sh), Aurora, Dawn, deko3d and libnx with devkitA64's GCC. The two use incompatible profile formats:
    • clang: -fprofile-instr-generate -> .profraw -> llvm-profdata merge -> -fprofile-instr-use=x.profdata (or -fprofile-generate/-fprofile-use for IR-level PGO).
    • GCC: -fprofile-generate -> .gcda per object -> -fprofile-use (what HD uses).
  • clang's profile runtime (libclang_rt.profile) is not built for aarch64-none-elf/libnx in our container. It has to be built against newlib, or replaced by a small dump of the counter sections (__llvm_prf_cnts/__llvm_prf_data/__llvm_prf_names) written from our own code on exit or on a debug-server command.
  • Thread pointer: libnx uses tpidrro_el0 and -mtp=soft; game units already use emulated TLS. HD had to disable value profiling (-fno-profile-values) because libgcov read tpidr_el0. Check whether the clang runtime has the same trap (-mllvm -disable-vp=true is the clang equivalent).
  • Regions: USA and EUR are separate builds (build_native.sh --eur), with different #if VERSION code; one profile per region or a merged one.

Steps:

  • Find out where the time goes first: game thread vs render worker (COS_PERF_EVERY, perf_scenes.py). If the game units dominate, start with clang PGO only; GCC PGO for Aurora/Dawn only if the render worker is the bottleneck.
  • Get an instrumented NRO to run and write its profile to the SD (runtime build or custom counter dump); fetch it with the debug server.
  • Training script: a warp tour over representative scenes with the debug server (like HD's pgo_train.py), keeping some scenes out to judge the result fairly.
  • Optimised build: -fprofile-instr-use in the launcher for game units, behind a CMake option (COS_PGO=OFF|GEN|USE, off by default), plus a check that warns on missing/stale profile (-Wprofile-instr-out-of-date, -Wprofile-instr-unprofiled).
  • Measure on the console at stock clocks (no OC, GPU at the official default profile), several boots per build, same scenes as before.
  • If worth it: decide where the .profdata lives (committed under switch/pgo/ is fine: it is counters from our own build of CC0 decomp code, no disc data; check that), when it is retrained (release checklist step after changes to game code or flags), and how the release script uses it.
  • Optional later: LTO / ThinLTO for the game units, which combines well with PGO.

Notes from HD to reuse: GCC's default -fprofile-use inflated hot code 29% (unrolling, peeling, tail duplication; check clang's equivalent), hot-section ordering via a linker script, and never judge speed on the instrumented build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions