perf(codegen): offset array reads earn the packed clone's numeric proof (41 → 16 ms) - #9279
Conversation
…of (41 -> 16 ms) `accumulator_rhs_is_numeric`'s `IndexGet` arm required a bare `Expr::LocalGet` index, so `a[k - 1]` was not numeric, the accumulator never earned its number proof, and every `+` in the enclosing expression lowered to a tag-test diamond over `js_dynamic_string_or_number_add` — three `is_number` tests and two helper calls on the cold arm, per iteration. That is the cost PerryTS#9060 and PerryTS#9091 already removed for the bare-counter form. Measured on the quiet host, `k < 4096`, `s = s + a[k] + a[k-1]` over a 4096-element `number[]`, 2000 reps: before 41 ms after 16 ms node 8 ms Nothing else moves: the plain-index rows stay at 7-8 ms and the length-bounded row stays at ~100 ms, since that one falls off both tiers for a different reason (PerryTS#9259, @ECS1's PerryTS#9274). Threaded per tier rather than widened. `collect_numeric_accumulators` takes `offset_reads_inlined` from each admission site: * range tier: `true`. It publishes `window_validated`, so its guard proved the whole window, and its hole-tolerant loads side-exit before producing a value — an offset read is lowered inline and yields a Number. * versioned and stable-packed tiers: `false`. Their offset reads take the generic path, which can produce `undefined`; admitting that as numeric would be a wrong answer rather than a missed optimisation. What the added tests do and do not guard, stated plainly because I checked: they cover the correctness of the shape this admits — an index that runs off either end, a hole inside the window, a non-numeric element, and a leading string that must concatenate rather than add, which is what a wrongly granted numeric proof would turn into a native `fadd`. They do NOT guard the per-tier flag. I flipped the versioned tier to `true` deliberately and all six still passed, because no loop reaches that tier with an offset read today — such a loop falls off both tiers (PerryTS#9259). The flag becomes observable when PerryTS#9274 lands, and flipping it then needs an `arr.length`-bounded fixture to be tested at all. perry-codegen 1843/0, perry-hir 596/0, `-D warnings` 0, local-binding-type audit OK, 34 packed-loop integration tests across 5 files including the 6 added.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe compiler now passes an ChangesPacked offset-read accumulator admission
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to This optimization admits offset array reads into the numeric fast path only for validated range loops, improving performance while retaining fallback behavior elsewhere. It is mergeable with owner awareness that future loop-tier changes must preserve the same bounds and hole-handling guarantees, since breaking that contract could produce incorrect numeric results. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description provides a detailed summary, technical rationale, related issue references, test coverage, validation results, performance data, and test limitations. It does not use the template headings or checklist, but it includes the required substantive information and is mostly complete.
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Merged. Validated on a shared branch with #9228, #9257, #9263, #9271, #9272, #9274, #9277, #9279 and #9280 — one build, one validation pass, then split back out and merged individually. Results across the batch:
One probe ( |
They are fmt-dirty on pristine main (42d0f45) from PerryTS#9274/PerryTS#9279; a stray `cargo fmt --all` picked them up. Reported separately, not fixed here.
They are fmt-dirty on pristine main (42d0f45) from PerryTS#9274/PerryTS#9279; a stray `cargo fmt --all` picked them up. Reported separately, not fixed here.
… on cargo fmt --check) (#9293) `cargo fmt --all -- --check` fails on pristine main (953a8bd): 6 hunks across `perry-codegen/src/stmt/loops.rs` and `perry-codegen/src/stmt/stable_packed_accumulator.rs`, from #9274/#9279. Reproduced on two machines with the pinned nightly toolchain. That gate is part of `lint`, so it is red on every open PR until this lands, and a check that is red on arrival teaches reviewers to ignore it — CLAUDE.md hazard 2. Pure `cargo fmt --all` output, no hand edits, no behaviour change. Claude-Session: https://claude.ai/code/session_01TE3JXAYXtdnKcLu8TCFWR6 Co-authored-by: Ralph Küpper <ralph3@skelpo.com>
They are fmt-dirty on pristine main (42d0f45) from PerryTS#9274/PerryTS#9279; a stray `cargo fmt --all` picked them up. Reported separately, not fixed here.
…9225's linear scan gated — cc --help −1.25% instructions, −2.37% cycles (#9291) * wip(runtime): address windows for the symbol and Uint8Array probes Hoist #9177's symbol address range out of is_registered_symbol_slow into is_registered_symbol as a RegistryAddrWindow, so the common negative answer costs no call; add the same window to is_uint8array_buffer. Both rejections are re-derived from the authoritative tables under debug_assertions. Not yet measured on cc --help. * perf(runtime): a monotone address FILTER in front of the symbol and class-prototype probes Round 2 (#9272) put an inline [lo, hi] address window in front of the buffer and typed-array probes. Measured against the four probes it named as follow-up, a window is the wrong shape for two of them and the right shape for one: is_registered_symbol 378,163 calls, window rejects 38.3% is_registered_class_prototype_object 26,290 calls, window rejects 54.0% is_uint8array_buffer 537,921 calls, window rejects 100% Symbols and class prototypes are ordinary GC-heap objects, so [lo, hi] grows to cover most of the heap. RegistryAddrFilter is the same monotone contract over a 1024-bit Bloom filter instead of a range; replaying each probe's real argument stream from a cc --help run, it rejects 99.58% and 99.05%. is_uint8array_buffer keeps the cheaper window (100% rejection, 0 true answers). Every rejection is re-derived from the authoritative table under debug_assertions, so a registration route added without admitting panics in the first test that touches it. * changelog: registry-probe address filter (symbol, class prototype) + Uint8Array window * test(runtime): keep TEST_SYMBOL_REGISTRY_PROBES meaning 'entry past the latch' Two sabotage checks in other suites defeat a cheaper upstream screen and require this counter to move; counting filter admissions instead made them fail. Filter admissions get their own counter, mirroring typedarray::TEST_TA_WINDOW_ADMITTED_PROBES. * test(runtime): the unregistered-scratch probe sweep covers the class-prototype probe too * docs(runtime): the descriptor-target scan comment's premise is false for every bundle (#9225) * docs(runtime): name the filter's saturation regime and the knob for it * fix(runtime): the debug audits use try_lock/try_read, not lock/read The rejection path never took either lock, so a blocking audit could hang on a caller the audited code would not have. Sabotage-checked: removing the admit from either registration funnel fails 1 test (symbol) and 3 tests (class prototype), so the audits demonstrably run. * docs(runtime): the symbol side's comments and the funnel's name say 'filter', not 'range' * docs(runtime): bits accrue per admission (the collector re-keys both tables); record the end-of-run false-positive rate * revert: unrelated cargo fmt reformat of two perry-codegen files They are fmt-dirty on pristine main (42d0f45) from #9274/#9279; a stray `cargo fmt --all` picked them up. Reported separately, not fixed here. * test: split the #8067 shape-authority tests out of parent_static.rs parent_static.rs was at 1992 lines on main and this PR adds 52, crossing the 2000-line cap. Extracts the inline shape_authority_tests_8067 module to a sibling under parent_static/; body unchanged. --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
The second of the two defects behind #9259. @ECS1's #9274 fixes the bound gap; this fixes the cap that sits under it. Independent changes, different files, additive.
The defect
accumulator_rhs_is_numeric'sIndexGetarm (stable_packed_accumulator.rs:45) required a bareExpr::LocalGetindex:So
a[k - 1]is not numeric → the accumulator never earns its number proof → every+in the enclosing expression lowers to a tag-test diamond overjs_dynamic_string_or_number_add. The hot loop is 47 instructions where the plain form is 11, and the reads are already hoisted — it is the add, not the array access:This is the cost #9060 and #9091 already removed for the bare-counter form.
Numbers
Quiet host,
k < 4096,s = s + a[k] + a[k-1], 4096-elementnumber[], 2000 reps:s += a[k] + a[k-1]Nothing else moves — plain-index rows stay at 7–8 ms, and the
arr.length-bounded row stays at ~100 ms because it falls off both tiers for the separate reason #9274 fixes. Every timing was taken with guard-call counts from the full function disassembly beside it, so "the code under test ran" is evidence rather than assumption.Threaded per tier, not widened
collect_numeric_accumulatorstakesoffset_reads_inlinedfrom each admission site:true. Publisheswindow_validated, so its guard proved the whole window; its hole-tolerant loads side-exit before producing a value. An offset read is lowered inline and yields a Number.false. Their offset reads take the generic path, which can produceundefined. Admitting that as numeric is a wrong answer, not a missed optimisation.@ECS1 corrected my original reasoning here — I had the two tiers' guarantees inverted, and on my own analysis I would have set the flag on the tier that cannot inline offset reads.
What the tests guard, and what they do not
They cover the correctness of the shape this admits: an index running off either end, a hole inside the window, a non-numeric element, and a leading string that must concatenate rather than add — which is precisely what a wrongly granted numeric proof turns into a native
fadd. All six node-identical.They do not guard the per-tier flag, and I checked rather than assumed. I flipped the versioned tier to
truedeliberately; all six still passed. No loop reaches that tier with an offset read today — such a loop falls off both tiers (#9259) — so the flag is currently unobservable in either position. It becomes live when #9274 merges, and flipping it then requires anarr.length-bounded fixture to be testable at all. I have left that in a comment at the call site rather than as folklore.Validation
-D warnings0, local-binding-type audit OKpacked_loop_abrupt_statements,loop_property_array_hoist,issue_8690_loop_versioned_arraylike,issue_8897_field_push_writeback, plus the 6 added)Refs #9259, #9274, #9060, #9091.
Summary by CodeRabbit
Bug Fixes
Tests
NaNor string results.