Repository navigation
perf(codegen): #8084's callee rooting costs pipeline 3.9%, consuming #8157's win on that row #8159
Description
Activity
Narrowed it (PR #8240) — and the narrowing recovers none of the 3.9%, because
pipelinenever reaches the arms #8084 touched.The attribution to the hunk is falsified
pipeline's inner loop isrec = stage(rec), wherestageis aStage-typed local. That does not lower throughtry_lower_closure_typed_local_call. It lowers throughlower_dynamic_closure_call(lower_call/console_promise.rs) — the #7154 lowering, which has rooted its callee and re-read its arguments below the unbox since long before #8084.Three independent confirmations, all on
07c8040bf:- The emitted IR for
gc-handoff/apps/pipeline.tsis byte-identical across a codegen change to exactly those three arms. Not "within noise" —cmpreturns 0. - Both
js_closure_call1sites in that IR are preceded bycall i64 @js_closure_unbox_callee_checked(...), which onlylower_dynamic_closure_callemits. js_new_function_constructandjs_closure_call_apply_with_spreadappear in the module only asdeclarelines. There is no call to either.
So the window {#8158, #8084} is right and the commit is right;
fix(codegen): root the callee across argument evaluation in three call armsis not the mechanism. It could not have been — that hunk emits zero instructions into this program.That also means the suggestion in the issue ("skipping the reread when nothing between adopt and use can allocate") would not have helped even if it worked perfectly. It does work — PR #8240 implements it, sabotage-checked from both sides — and it moves
pipelineby +0.02%,interp−0.04%,iso_miss−0.05%,asyncpipe+0.11%,shapes−0.18%. Same protocol as this issue's: identical runtime archives on both arms, per-arm cache dirs,PERRY_NO_AUTO_OPTIMIZE=1, min-of-5, stdout sha-identical. Where it does move something is zod, the population that actually has these shapes: dep-native live bundles 36611 → 36598.Where the 3.9% probably is
#8084 is ~2,500 lines beyond that hunk, and most of it is GC runtime that a program allocating ~1.4M objects exercises constantly:
gc_map.rs(+355),gc/roots/stack_maps.rs+_decode+_sections(~1,660 changed),gc/copying.rs+copying_pointer_set.rs,pin.rs(+209),tenuring.rs(+41),object/native_call_method.rs,object/this_binding.rs.Sharpest suspect, stated as a hypothesis I have not measured:
js_implicit_this_set. It ispipeline's hottest FFI function — the IR has 4 call sites to it against 2 closure-call sites (a save and a restore per call), and the dynamic count is ~2.9M calls per run (400 rounds x 900 records x 3 stages, plusidf). #8084 turned it from a bare TLSreplaceintothis_set_check(value, …); replace; this_set_check(previous, …), and each check opens with aOnceLock::get_or_init— an atomic load and a branch on the fast path, paid twice per call, for a diagnostic (PERRY_GC_THIS_SET_CHECK) that is default-off.Back-of-envelope that is ~1%, not 3.9%, so it is at most part of it — but it is a default-off diagnostic sitting on the hottest path, which is the shape CLAUDE.md's GC-knob policy exists to prevent, and it is cheap to test: one runtime rebuild, no codegen change.
Given the IR result, I'd bisect within #8084 by file rather than re-measure the whole commit — you already know the answer is not in the part everyone assumed.
Two main-side reds found on the way
Both reproduce on clean
07c8040bfwith no PR applied (both arms of my A/B report them identically), and both were invisible until #8207 fixed--audit-poll-capable, because the job aborted before reaching the corpus steps:- curated-native is red: 1
stalehazard against--max-stale 0—test_gap_class_forward_capture_6523::perry_closure_…__7,unmasked->js_box_set, MOVING viajs_object_get_field_by_name_f64— and seeded violations 39/40 caught, 1 MISSED where CI requires 40. - dep-native reads
unrooted 2, not the3the workflow budgets. Slack nobody re-measures is where the next regression lands green. I deliberately did not tighten it in perf(codegen): compute the callee-rooting window instead of hardcoding it (#8159) #8240: the number is LLVM-version dependent and I measured with homebrewopt, not CI's, so lowering a budget I cannot verify in CI is how the gate goes red for the next person. Worth CI measuring it and ratcheting.
(dep-native itself is green and non-vacuous on clean main: 12,957 functions / 81 modules, 49,114 safepoints, seeded 40/40 caught,
--self-test OK.)Leaving this issue open rather than closing it with #8240 — the PR answers the "can it be narrowed?" question and records the cost, but the 3.9% is still unattributed, and that is what the title is about.
- The emitted IR for
- added a commit that references this issue
on Aug 16, 2026
Summary
pipelineis the only corpus row that got slower across 2026-08-15's twenty merges: +0.28% instructions onmain@d6d7d0efeversus83b6b8c69. Every other row is flat or faster, several enormously.The number is small but it is 2× the measured noise floor (±0.15%, established by compiling
maintwice into separate cache/out dirs and confirming all 22 binaries byte-identical), and the run ranges do not overlap: base 3.7881–3.7936 G, main 3.8015–3.8053 G. Peak RSS is unchanged.It is a give-back, not a shortfall
Building the intervening merge points attributes it to a single step:
pipelineinstructions83b6b8c69(session start)8b1b4b909(#8157 merge)53d63aad2(#8084 merge)d6d7d0efe(today's main)#8157 delivered its full claimed −3.52% here. The next step gave all of it back and a little more.
Every other row moves by ≤0.03% across that same
at8157 → at8084step, so this is one localized change, not drift.Attribution
The window is {#8158, #8084}. #8158 is a 7-line addition to
BUILD_CACHE_ENV_VARSand cannot change emitted code, which leaves #8084 — specificallyfix(codegen): root the callee across argument evaluation in three call arms, touchingnew_dynamic.rs,call_spread.rsandearly_branches.rs.pipelineis spread- and call-heavy, so the mechanism fits: each rooted callee adds anopen_rooted_group/adopt/reread/releasesequence around a call site.This is not a request to revert
#8084 fixes a real moving-GC rooting bug — a callee held in a bare register across argument evaluation is a pre-move address, and the dependency-scale corpus found 16 sites of that shape. Correctness wins over 3.9% on one row, and #8084 should stay.
Filing it because nobody wrote the cost down. A correctness fix that silently consumes an entire optimisation's win on a row is exactly the thing that later gets rediscovered as an unexplained regression, and #8157's
pipelinewin currently looks like it never happened.Worth looking at
The rooting sequence is emitted per call site. Whether it can be narrowed — e.g. skipping the reread when nothing between adopt and use can allocate, which is a property the tree already computes elsewhere for loop polls (
loop_purity::loop_may_allocate) — is the obvious question. That would keep the soundness property while paying for it only where a collection is actually reachable.Measurement protocol:
/usr/bin/time -l, min-of-5, arms interleaved round-robin with order reshuffled each round, per-armPERRY_RUNTIME_DIRandPERRY_CACHE_DIR,PERRY_NO_AUTO_OPTIMIZE=1, all arms'libperry_runtime.acmp-verified to differ, stdout sha256 identical across arms.Refs #8084, #8157.