Skip to content

Cheaper label checkpoints in the generated code (Thor main thread) - #222

Merged
YuutaTsubasa merged 4 commits into
mainfrom
claude/perf-generated-code
Oct 20, 2026
Merged

YuutaTsubasa merged 4 commits into
mainfrom
claude/perf-generated-code

Conversation

@YuutaTsubasa

Copy link
Copy Markdown
Owner

Generated guest code is ~32% of a race frame's main thread on the AYN Thor (simpleperf, release 0.10.13). Label checkpoints and enter_function make up a quarter of that time. They also make the code around them slower: the permit call on a checkpoint's rare path is an ordinary call as far as the compiler knows, so guest registers are stored before every label and loaded again after it, and loops keep their counters in memory.

Changes

  • SFR_HIDDEN_CHECKPOINT (CMake option, default ON; only AArch64 Linux and Android): a label checkpoint's permit call is made from a non-volatile asm statement through sfr_checkpoint_trampoline. The count comes back in x0, and exceptions are caught and rethrown. __builtin_assume_separate_storage tells the compiler the thread state is separate from the context and guest memory. sfr::guest_checkpoint(ctx) passes the function's base through a macro. As a result, loops without calls keep guest registers and the count in host registers.
  • GPR save/restore hooks (6% of the thread) now use the base the generated code passes them. Before, they read active_memory->base(): three dependent loads, and the helpers' hottest instruction.
  • Only loop heads keep a label checkpoint (29,100 of 172,524 labels). Function entries and loop heads still checkpoint, and guest-visible behaviour is unchanged. The generator now emits only those checkpoints. scripts/loop_head_checkpoints.py applies the same rule to an existing output directory without regenerating it, and --check verifies the result.
  • docs/performance.md (2026-10-20 generated code): the profile breakdown, the patterns and flags evaluated, and ranked proposals.

Thor A/B (Dolphin Resort moving race, HOST_THREADS frames 13000-21000, versionCode 61 test builds)

Build drawn_ms undrawn_ms cpu_ms skipped /8040
baseline (0.10.13 source) 16.13 / 15.97 9.78 / 9.86 14.47 / 14.33 320 / 211
hidden checkpoint + helpers' base 15.75 / 16.03 8.90 / 9.40 13.93 / 14.18 80 / 160
+ loop-head checkpoints only 15.96 8.31 13.81 39
no label checkpoints (upper bound, not shippable) 16.01 7.28 13.52 12
SFR_TEST_RACE_DRAW_MS=10 (~4,700 undrawn frames each) undrawn_ms cpu_ms
baseline 9.53 / 8.99 11.38 / 10.88
hidden checkpoint + helpers' base 9.00 11.13
+ loop-head checkpoints only 8.41 / 8.60 10.64 / 10.86

Tests

  • Thor races: speed 0.999, screenshots render correctly.
  • Windows:
    • Vulkan race with the validation layer (loader confirms the layer): no messages, on both the shipped and the loop-head-only code.
    • D3D12 races reached their present limit on both.
    • ctest: 165/165.
  • Linux (WSL) ctest: 158/158.
  • Python: 356 OK, 11 skipped (they need the game files).
  • Android arm64 build.

Shipping the loop-head part with the current shipped generated code needs two steps at release time:

  1. python scripts/loop_head_checkpoints.py <out/recomp-midasm/diagnostic> <copy>, then the same command with --check to verify the copy.
  2. Build with -DSFR_DIAGNOSTIC_DIR=<copy>.

Without the copy, the build still gets the hidden checkpoint and the helper change.

🤖 Generated with Claude Code

YuutaTsubasa and others added 3 commits October 20, 2026 21:28
…nerated base

The generated code checkpoints at every label (172,524 of them, 17% loop
heads). The permit call on the checkpoint's rare path is an ordinary call
as far as the compiler knows, and the context is passed to every callee,
so every guest register in a host register had to be stored before each
label and reloaded after it; a loop without calls kept its counter and
pointers in memory, each iteration a store and a reload. On AArch64 Linux
and Android (SFR_HIDDEN_CHECKPOINT, CMake option, default ON) the call is
now made from a non-volatile inline asm statement that names no memory,
through a trampoline; the count the permit sets comes back in x0 and is
written back where the compiler sees it, and the permit's exceptions are
caught on the far side and rethrown. The checkpoint also tells the
compiler the thread state lies apart from the context and guest memory
(the generated sfr::guest_checkpoint(ctx) now passes the function's base
through a macro), so such a loop keeps its count in a register too.

The GPR save/restore hooks (6% of the AYN Thor main thread) found guest
memory's base through active_memory: three dependent loads before the
first register could move, their hottest instruction. They now use the
base the generated code passes them, which is the same pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every label of the generated code called sfr::guest_checkpoint(ctx), but
only 29,100 of the 172,524 are loop heads (a label a goto at or after it
returns to, switch cases included). A forward-only label is passed at
most once between two function entries, which checkpoint themselves,
so its checkpoint only counted down; two thirds of the checkpoint time
on the AYN Thor's main thread was at such labels, and each one is a
load, a decrement and a store on one count that every checkpoint of the
thread chains through.

The generator now emits the checkpoint only at loop heads.
scripts/loop_head_checkpoints.py does the same to an existing output
directory (the shipped generated code is not regenerated) and --check
verifies the copy is the input less those lines. Guest-visible behaviour
is unchanged; the permit's count now spans more guest work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profile of the generated code (statement and pattern breakdown), what was
changed and measured (hidden label checkpoints, the helpers' base,
loop-head-only checkpoints), how to ship the loop-head part with the
current generated code, and the remaining proposals ranked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 16:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@YuutaTsubasa
YuutaTsubasa merged commit f5ae365 into main Oct 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants