perf(codegen): revive the class-field versioned loop for canonical-i32 counters - #7425
Merged
Conversation
added 4 commits
August 5, 2026 10:46
…cal-i32 counters Repsel Phase 1 made the canonical i32 slot the ONLY storage for a proven-integer local, so such a local has no `ctx.locals` entry. The #5093 matcher gated its counter and its bound on `ctx.locals`, so it matched nothing. Also teach the sloppy class-field store (#7423) about the loop fact, so the fast clone stays call-free.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughChangesClass-field loop guard hoisting
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant LoopMatcher
participant FastPreheader
participant FastClone
participant SlowClone
LoopMatcher->>FastPreheader: Match readable loop slots
FastPreheader->>FastClone: Enter after class-field guard
FastClone->>FastClone: Validate and store finite raw-f64 value
FastClone->>SlowClone: Side-exit invalid value
Possibly related PRs
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7287.
09_method_calls82ms → 10ms — parity with Node (8.2×, from 8× behind).The bug: an optimization that had never once fired
Perry already had this hoist.
emit_class_field_loop_preheader_check+lower_class_field_versioned_forwere written for this exact benchmark — the comment atstmt/loops.rs:4657names it: "counter.value = counter.value + 1after method inlining". The HIR for09_method_callsis precisely the shape its matcher describes.It matched nothing. Instrumenting every bail site: repsel Phase 1 (
expr/slot_rep.rs) made the canonical i32 slot the only storage for a proven-integer local — registered inctx.local_slot_reps, with noctx.localsentry. The matcher gated both its counter and its bound onctx.locals.contains_key(..), so it declined every loop.Turning Phase 1 off doesn't recover it either: the counter regains
ctx.locals, but a barei++never earns an i32 shadow, which the lowering separately requires. Dead in both configurations, with zero tests — CLAUDE.md's fourth failure mode. A sibling matcher had already been repaired for exactly this (local_bound_storage_accessible); this one was missed.Reviving the matcher alone still changed nothing: sloppy module scope routes the store through
try_lower_sloppy_class_field_raw_store(#7423, merged today), which had no loop-fact branch, so the fast clone contained ajs_put_value_setcall and the call-freeness check branched around it. Both halves were needed.The volatile-load hypothesis measured zero
I briefed this as the strongest lead —
load volatile @PERRY_CLASS_FIELD_INLINE_GUARD_DISABLEDcan't be hoisted by LLVM, so it looked like it pinned the chain. Tested directly: editingload volatile→loadand re-runningopt -O3gives 71 hot-path instructions either way. Marking every guard-arm callmemory(none) nounwind willreturnas well still leaves both deref blocks in the loop. LLVM won't speculate loads out of a conditionally-executed block whose receiver it can't prove dereferenceable. The "do not de-volatilize the gate" note atclass_field_inline_guard.rs:17-26stands.Verification
opt -O3per iteration09_method_callsThe 29-instruction guard chain now sits in
class_field_loop.preheader.deref, run once, and LLVM promotes the field load into a loop-carried register.definePropertynon-writable/mid-run,freezebefore/mid-run,seal, own accessor, prototype accessor,deletebefore/mid-run, subclass with different layout, receiver alternating shape,Infinity/NaNmid-loop side exits, zero/one-trip — byte-identical to Node in both arms; the diff was mutated once to confirm it can failperry-codegen --lib635/635; 68 class/field/proto gap tests plus a wider 187-test set identical in both arms;perry-runtime --lib3 failures aregc::tests::teardown::map_set_*parallel flakes — the branch touches zero perry-runtime filesSeparately worth filing
reach_frozensurfaced that Perry silently no-ops a frozen field write where Node throwsTypeError— the inliner drops the class body's strictness. Pre-existing and identical in both arms, so out of scope here.Summary by CodeRabbit
Performance
Bug Fixes
Tests