Skip to content

perf(gc): bind scalar-replacement root slots once per frame, not once per store - #7013

Merged
proggeramlug merged 1 commit into
mainfrom
perf/hoist-gc-roots-per-safepoint
Jul 29, 2026
Merged

perf(gc): bind scalar-replacement root slots once per frame, not once per store#7013
proggeramlug merged 1 commit into
mainfrom
perf/hoist-gc-roots-per-safepoint

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Hoists the per-store js_shadow_slot_bind that #7007 emits for scalar-replaced
object fields and array elements out to the function-entry setup, leaving at the
store only the incremental-mark shading barrier — emitted inline and guarded, so
the common path is a load, a compare and a not-taken branch instead of a
TLS-touching call.

Why the bind is loop-invariant. A bind sets slot_ptrs[idx] = alloca,
mirrors stack[idx] = *alloca, sets active[idx], and runs the root barrier.
The alloca is entry-hoisted and never moves, and both readers of a bound slot
(visit_shadow_stack_root_slots, js_shadow_slot_get) dereference
slot_ptrs[idx] in preference to the stack[idx] mirror — so the mirror is dead
storage and only the barrier is per-store work. Same treatment
enable_persistent_shadow_slot_for_array_alias already gives a
const item = arr[i] alias.

The hoist does not move when the rooted value is read. The collector reads
the alloca at collection time, exactly as it did when the bind sat at the store.
Nothing is snapshotted into a register and re-read later, so the value a
safepoint observes is unchanged — what disappears is a redundant copy, not an
observation point.

Measured on a quiet Mac mini (load 1.9), pinned Node 26.5.0, 10 M iterations
of { s: mkStr(i), y: (i>>3) & 1023 }, best-of-5, arms interleaved:

arm best
pre-#7007 (no rooting) 123 ms
#7007 as merged (bind per store) 164 ms
this change (bind per frame) 126 ms

41 ms of rooting cost → ~3 ms, 93 % recovered (~4.1 ns/store → ~0.3 ns).
Binary size byte-identical. The #6997 numeric gate is untouched: a proven-numeric
field still takes no slot, no call, no frame growth — and now no entry bind.

The one soundness obligation the hoist creates. Binding at entry makes the
slot active from function entry, so the collector dereferences the alloca
before any store reaches it and on paths where none ever does. An uninitialized
alloca would hand the root-word decoder stack garbage that can pass
is_plausible_heap_addr. The object-literal, anonymous-shape and split() paths
already stored undefined into their slots in entry_allocas; the
array-element and fused-uppercase-receiver paths did not, and now do.

Correctness. gc_repsel_matrix.sh --arms all --pressure 8 vs pinned Node
26.5.0: PASS=229 UNVER=190 XFAIL=1 FAIL=20 over 440 cells (22 rows,
post-#7011) — main's published 460-cell baseline (240/199/1/20) minus exactly
the row #7011 de-duplicated, which contributed 11 PASS and 9 UNVER. No cell
changed state. Both FAIL files are the #6981 pair. test_gap_repsel_scalar_replaced_locals
is PASS on all 20 arms including the evacuating ones, where the arm was
confirmed live (copied_objects 261 251–379 072, output byte-identical to the
oracle) — the hoisted binding is still rewritten through slot_ptrs[idx] when
the collector relocates its referent.

Tests. Four new codegen-contract tests, each verified to FAIL against the
pre-hoist compiler: one bind per slot rather than per store; the bind in the
entry block ahead of the storing loop and after js_shadow_frame_push
(binding before the push would write the caller's frame); a shading barrier at
every store; and the entry initialization the hoist depends on.

Changeset fragment follows in #7014 — this PR was merged from a WIP commit
before the fragment was attached.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Scalar-replaced slot rooting now binds shadow slots once in function entry, emits a persistent root barrier after each eligible store, initializes rooted allocas to undefined, and adds LLVM IR tests for ordering, barrier counts, initialization, and numeric-only exclusions.

Changes

Scalar-replaced slot rooting

Layer / File(s) Summary
Persistent root barrier API
crates/perry-codegen/src/expr/shadow_slot.rs, crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/expr/scalar_slot_root.rs
emit_persistent_shadow_root_barrier is crate-visible and re-exported for scalar-slot codegen.
Entry binding and alloca initialization
crates/perry-codegen/src/expr/scalar_slot_root.rs, crates/perry-codegen/src/stmt/let_stmt.rs
Scalar-replaced slots bind once during entry setup, retain per-store barriers, and initialize capture and array element allocas with nanboxed undefined.
Generated IR regression coverage
crates/perry-codegen/tests/scalar_replaced_slot_roots.rs
Tests verify bind hoisting, barrier emission and guards, initialization ordering, and numeric-only exclusions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScalarSlotRoot
  participant EntrySetup
  participant ShadowSlot
  participant LLVMIRTests
  ScalarSlotRoot->>EntrySetup: schedule one js_shadow_slot_bind
  EntrySetup->>ShadowSlot: bind initialized alloca
  ScalarSlotRoot->>ShadowSlot: emit per-store root barrier
  LLVMIRTests->>ScalarSlotRoot: inspect generated LLVM IR
Loading

Possibly related PRs

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is far shorter than the required template and omits Summary, Changes, Related issue, Test plan, and Checklist. Fill in the repository template with Summary, Changes, Related issue, Test plan, optional screenshots/output, and the checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: hoisting scalar-replacement root binding to function entry.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/hoist-gc-roots-per-safepoint

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry-codegen/tests/scalar_replaced_slot_roots.rs (1)

441-450: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the requested benchmark matrix.

The PR objective asks for measurements and matrix results in a comment; this section only states an approximate per-iteration cost. Include the measured configurations and before/after results.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/scalar_replaced_slot_roots.rs` around lines 441 -
450, Expand the comment above the per-slot binding test to include the requested
benchmark matrix, listing each measured configuration and its before/after
results. Replace the approximate “~4 ns per iteration” statement with the actual
measured data while retaining the explanation that the bind is loop-invariant
and scoped per slot.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-codegen/tests/scalar_replaced_slot_roots.rs`:
- Around line 441-450: Expand the comment above the per-slot binding test to
include the requested benchmark matrix, listing each measured configuration and
its before/after results. Replace the approximate “~4 ns per iteration”
statement with the actual measured data while retaining the explanation that the
bind is loop-invariant and scoped per slot.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 938f083d-6cc4-46cc-a298-f94c50de6c3b

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad27a1 and 9374131.

📒 Files selected for processing (5)
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/scalar_slot_root.rs
  • crates/perry-codegen/src/expr/shadow_slot.rs
  • crates/perry-codegen/src/stmt/let_stmt.rs
  • crates/perry-codegen/tests/scalar_replaced_slot_roots.rs

@proggeramlug
proggeramlug merged commit df9c7d2 into main Jul 29, 2026
29 of 33 checks passed
@proggeramlug
proggeramlug deleted the perf/hoist-gc-roots-per-safepoint branch July 29, 2026 16:20
proggeramlug added a commit that referenced this pull request Jul 30, 2026
…7014)

PR #7013 was merged from a WIP commit before its changeset fragment was
added. This supplies it: root cause, the measured 41ms -> ~3ms recovery,
the entry-initialization obligation the hoist creates, and the 440-cell
matrix result.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant