Splitting the guest-memory accessors so V8 inlines their fast paths — a null

Measured 2026-08-31 on branch worktree-agent-aefd5647b019c4c9d (a9131389). Verdict: the shape change does exactly what it was designed to do and buys 0% throughput. Do not land it for performance. The interesting part is what it rules out.

The hypothesis

$g2w (2708 static call sites), $gs32 (2558) and $gl32 (2237) were each a 3-6 op fast path fused to a long cold tier — $g2w's ends in a scan over the sparse VirtualAlloc table. V8's wasm inliner prices the whole callee against a growth budget (--wasm-inlining-factor=3, floor --wasm-inlining-min-budget=50), so a caller that only ever needs the three-instruction fast path pays a call instead. Raising the floor to 600 was measured at ~8% user CPU (memory: V8 wasm inlining budget), and no browser accepts the flag — so the portable form is to make the fast path its own tiny function.

Done in src/03-registers.wat: each accessor is now a wrapper (fast-path test plus result) over a $NAME_slow callee that only a miss reaches.

The mechanism landed

node --trace-wasm-inlining, Heroes II demo, 40000 batches, counting the verdict at every candidate site:

callee before: size / inlined / denied after: size / inlined / denied
$g2w 360 B — 9 sites / 18 denied 35 B — 55 / 12
$gl32 104 B — 18 / 42 37 B — 51 / 9
$gs32 117 B — 5 / 55 48 B — 39 / 22

Weighted by V8's own call counts on the split build, $g2w is inlined at sites carrying 2.42M calls against 0.66M denied — the reverse of before.

And bought nothing

Fixed work, interleaved A/B, user CPU (wall clock is inadmissible on this box — it sat at load 5-17 throughout). All reps, not just medians:

workload base split
Caesar III demo, 40000 batches 92.06, 70.58, 70.89 (med 70.89) 70.07, 72.70, 70.75 (med 70.75)
Heroes II demo, 300000 batches 34.01, 36.33, 33.50, 34.83 (med 34.42) 33.60, 33.80, 35.07, 36.06 (med 34.44)
Heroes II, paired inside the thermometer rounds 45.03 / 53.60 / 49.49 45.40 / 54.67 / 49.61

The paired rounds are the cleanest read: same round, same load, split is +0.8%, +2.0%, +0.2% — flat to a hair slower. Startup (sol, 2000 batches, 5 reps) is flat as well: base median 1.62s, split 1.62s, so this is not the wasm-opt failure mode (that one tripled lazy-compile time).

tools/bench-loops.js cannot arbitrate this: it only ever loads build/wine-assembly.wasm, so two artifacts cannot be interleaved inside one process and its ±1% floor does not apply. Run as separate processes over three alternating rounds the per-round deltas were -4.5%, +9.8%, -25% on lut — load, not signal.

The thermometer did not collapse — and that is the finding

If the split had captured the budget's win, --wasm-inlining-min-budget=600 would stop helping the split build. It still helps, as much as ever (same-round ratios):

round base stock → flag split stock → flag
1 45.03 → 41.22 (-8.5%) 45.40 → 40.18 (-11.5%)
2 53.60 → 46.47 (-13.3%) 54.67 → 43.20 (-21.0%)
3 49.49 → 45.47 (-8.1%) 49.61 → 40.31 (-18.7%)

So the ~8-13% the flag is worth is not the memory accessors. With the accessors inlined, the calls the budget still refuses, weighted by call count (node tmp/inline-denied-top.js, split build, stock V8):

callee size calls at denied sites calls at inlined sites
$next 106 B 3,154,969 58,067
$get_reg 69 B 1,619,273 740,426
$set_reg 85 B 1,324,740 0
$invalidate_code_write 43 B 828,446 573,540
$branch_end 100 B 388,936 38,308
$jcc_end 80 B 147,272 0

$next at 98% denied and $set_reg at 100% denied are the whole remaining prize. That is a register/dispatch-shape problem, not a memory-translation one, and it is where the next attempt at this lever should go.

What to do with the branch

The split is behaviour-preserving (sol and notepad captures pixel-identical; test-x86-ops, test-mem-utils-dib-g2w, test-thread-manager, test-wat-memory-map, test-gdi-p0-p1, the four sparse/boundary suites, test-copy32-bounded-run and test-aoe2-span-prefix all pass) and it makes the hot path readable, so it is safe to take on style grounds. It is not a performance change and must not be quoted as one.