Skip to content

perf: pipeline's +3.95% at #8084 is unattributed — it is NOT the callee rooting (IR is byte-identical) #8243

Description

@proggeramlug

Splitting this out of #8159, which is closed by #8240: the narrowing question is answered, the 3.9% is not.

What is established

pipeline retired +3.95% instructions at 53d63aad2 (#8084) versus 8b1b4b909 (#8157) — measured in #8159, run ranges non-overlapping, 2x the noise floor, every other row flat.

#8159 attributed that to fix(codegen): root the callee across argument evaluation in three call arms. That is falsified. Three independent confirmations on 07c8040bf:

  1. A codegen change to exactly those arms (perf(codegen): compute the callee-rooting window instead of hardcoding it (#8159) #8240) leaves the emitted IR for gc-handoff/apps/pipeline.ts byte-identicalcmp returns 0, not "within noise".
  2. Both js_closure_call1 sites in that IR are preceded by call i64 @js_closure_unbox_callee_checked(...), which only lower_dynamic_closure_call (lower_call/console_promise.rs) emits. That is the GC: evacuating minor drops an old-to-young field[1] edge, crashing with 'value is not a function' #7154 lowering, which has rooted its callee and re-read its arguments below the unbox since long before gc: diagnostics, two rooting fixes, and the ungated corpus/lowering cell (#7803 investigation) #8084.
  3. js_new_function_construct and js_closure_call_apply_with_spread appear in the module only as declare lines. There is no call to either.

Corpus-wide, #8240 measures pipeline +0.02%, interp −0.04%, iso_miss −0.05%, asyncpipe +0.11%, shapes −0.18% — identical runtime archives on both arms, per-arm cache dirs, PERRY_NO_AUTO_OPTIMIZE=1, min-of-5, stdout sha-identical.

So the commit attribution stands and the hunk attribution does not. The cost is in #8084's other ~2,500 lines.

Search space, ranked

Most of the remainder is GC runtime, which a program allocating ~1.4M objects exercises constantly:

  • gc/roots/stack_maps.rs + _decode.rs + _sections.rs — ~1,660 lines changed; runs on every root scan
  • gc/copying.rs (−306) + gc/copying_pointer_set.rs (+248) — the copying minor's pointer set, restructured
  • gc_map.rs (+355, codegen) — bigger/differently-shaped maps cost scan time
  • gc/pin.rs (+209), gc/tenuring.rs (+41)
  • object/this_binding.rs, object/native_call_method.rs

Sharpest suspect, hypothesis not measurement: js_implicit_this_set. It is pipeline's hottest FFI function — the IR has 4 call sites against 2 closure-call sites (a save and a restore per call), so ~2.9M dynamic calls per run (400 rounds x 900 records x 3 stages, plus idf). #8084 turned it from a bare TLS replace into this_set_check(value, …); replace; this_set_check(previous, …), and each check opens with a OnceLock::get_or_init — an atomic load plus 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 at most part of it. But a default-off diagnostic on the hottest path is the shape CLAUDE.md's GC-knob policy exists to prevent, and it is cheap to test: one runtime rebuild, no codegen change. I had that experiment building when I stopped; it is not measured.

Given the IR result, bisect within #8084 by file rather than re-measuring the commit — the answer is known not to be in the part everyone assumed.

Two main-side reds found on the way

Both reproduce on clean 07c8040bf with no PR applied (both arms of the #8240 A/B report them identically). Both were invisible until #8207, because gc-root-dominance aborted earlier at --audit-poll-capable and never reached the corpus steps:

  • curated-native is red. One stale hazard against --max-stale 0: test_gap_class_forward_capture_6523::perry_closure_test_gap_class_forward_capture_6523_ts__7, unmasked->js_box_set, MOVING via js_object_get_field_by_name_f64. And seeded violations 39/40 caught, 1 MISSED, where the workflow requires all 40 — that second one is the "can the checker still fail" arm, so it matters more than the first.
  • dep-native reads unrooted 2, not the 3 the workflow budgets. A ratchet only ratchets if it tracks the actual value; one new unrooted site would currently land inside budget and go green. Not tightened in perf(codegen): compute the callee-rooting window instead of hardcoding it (#8159) #8240 on purpose — the number is LLVM-version dependent and was measured with homebrew opt, not CI's, so lowering a budget that cannot be verified in CI is how the gate goes red for the next person. Worth CI measuring 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.

Refs #8159, #8084, #8240, #8207.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions