Skip to content

perf(codegen): #6794 follow-up (b) — skip redundant shadow-slot clears in region fast copies#6846

Merged
proggeramlug merged 3 commits into
mainfrom
perf/6794-b-shadow-clear-dedup
Jul 26, 2026
Merged

perf(codegen): #6794 follow-up (b) — skip redundant shadow-slot clears in region fast copies#6846
proggeramlug merged 3 commits into
mainfrom
perf/6794-b-shadow-clear-dedup

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

#6794 follow-up (b): skip redundant shadow-slot clears in region fast copies

Follow-up (a) (#6844) shipped the region i32-chains. Profiling bcryptjs
compareSync under Perry then shows the #1 runtime symbol is _tlv_get_addr
— the thread-local access behind the GC shadow-frame/slot ops. This PR removes the
biggest chunk of that traffic that is provably redundant.

The redundancy

A masked-window region fast copy flow-refines its numeric locals to Number and
suppresses their per-statement shadow updates — emit_shadow_slot_update_for_expr
returns early for any masked_region_scalar_locals member. So once such a local's
shadow slot is cleared to 0 at its refinement point, no later write ever sets it
again
, yet the per-statement clear machinery re-emits js_shadow_slot_set(slot, 0) after every write. Each is a TLS hit (_tlv_get_addr) doing nothing.

The fix

Track slots a copy has already cleared for a still-suppressed local
(suppressed_cleared_shadow_slots); emit_shadow_slot_clear skips them. Added
right after the first clear, removed the instant a local leaves
masked_region_scalar_locals, dropped at copy end — so only region fast copies
change; non-region code is byte-for-byte identical
.

Effect (16-round _encipher shape)

shadow op before after
ta_i32 fast copy js_shadow_slot_set 12 4
whole function js_shadow_slot_set 29 21

One clear per suppressed slot instead of one per write. (The entry frame push +
param binds remain — eliding those for the fast path is a larger, GC-sensitive
follow-up.)

Correctness

Sound: a skipped clear re-zeros a slot that provably already holds 0, so GC roots
are unchanged. Validated byte-for-byte vs Node on region16 / bcryptjs / the
i32-overflow / toint32 / typed-array / crypto gap suite, and under
PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1
(panics on a missed or
stale root). New gap test test_gap_region_shadow_clear_gc.ts drives region
functions while allocating each iteration so real collections fire with region
frames live.

Wall-clock A/B was not measurable this run (the shared box was at load ~100 from
another build); the op-count reduction is the definitive metric.

Summary by CodeRabbit

  • Performance

    • Reduced redundant shadow-clear operations during optimized masked-window region fast copies, cutting repeated internal cleanup work.
  • Bug Fixes

    • Maintained GC safety and byte-identical observable behavior while avoiding unnecessary redundant shadow-slot updates.
  • Tests

    • Added a regression test that runs a bcryptjs-shaped encipher loop with forced garbage collection and stress allocation to verify behavior under evacuation.

Ralph Küpper added 2 commits July 26, 2026 11:27
…s in masked-window region fast copies

A masked-window region fast copy flow-refines its numeric locals to Number and
suppresses their per-statement shadow updates (`emit_shadow_slot_update_for_expr`
returns early for a `masked_region_scalar_locals` member). Once such a local's
shadow slot is cleared to 0 at the refinement point, suppression guarantees no
later write ever sets it again — so every subsequent per-statement clear
(`js_shadow_slot_set(slot, 0)`) is a redundant no-op. Each of those touches a
thread-local, i.e. an `_tlv_get_addr` call, which profiling shows is the #1
runtime symbol in bcryptjs `_encipher` (the shadow-frame/slot TLS traffic is the
dominant residual after follow-up (a)).

Track the slots a copy has already cleared for a suppressed local
(`suppressed_cleared_shadow_slots`); `emit_shadow_slot_clear` skips them. Entries
are added right after the first clear, removed the instant a local leaves
`masked_region_scalar_locals` (un-refine), and the set is dropped at copy end, so
only region fast copies are affected and non-region code is byte-for-byte
unchanged.

Effect on the 16-round `_encipher` shape: ta_i32 fast copy `js_shadow_slot_set`
12 -> 4 (one clear per suppressed slot instead of one per write); whole function
29 -> 21. Sound: the skipped clears re-zero a slot that provably already holds 0,
so GC roots are unchanged.

Validated byte-for-byte vs Node on region16 / bcryptjs / the i32-overflow /
toint32 / typed-array / crypto gap suite, AND under PERRY_GC_FORCE_EVACUATE=1
PERRY_GC_VERIFY_EVACUATION=1 (which panics on a missed/stale root). New gap test
drives region functions while allocating each iteration so real collections fire
with region frames live.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 706b27e0-696f-472c-88bf-ae27c473711f

📥 Commits

Reviewing files that changed from the base of the PR and between 110f967 and 32e5efe.

📒 Files selected for processing (1)
  • test-files/test_gap_region_shadow_clear_gc.ts

📝 Walkthrough

Walkthrough

Masked-window region fast copies now track cleared shadow slots, skip redundant clears, and reset suppression at refinement and copy boundaries. All FnCtx construction paths initialize the tracking set, and a GC regression test exercises the updated codegen path.

Changes

Shadow-slot clear suppression

Layer / File(s) Summary
FnCtx suppression state
crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/codegen/{closure,entry,function,method}.rs
FnCtx declares suppressed_cleared_shadow_slots and initializes it for closure, entry, function, instance-method, and static-method compilation.
Masked-window clear suppression and validation
crates/perry-codegen/src/stmt/masked_window_region.rs, crates/perry-codegen/src/expr/shadow_slot.rs, test-files/test_gap_region_shadow_clear_gc.ts, changelog.d/6846-region-shadow-clear-dedup.md
Masked-window lowering records cleared slots, removes them when refinements end, and clears the set after fast copies; shadow-slot emission skips recorded slots, with GC regression coverage and changelog documentation added.

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

Possibly related PRs

  • PerryTS/perry#6794: Introduced the masked-window region and shadow-slot suppression paths updated here.
  • PerryTS/perry#6844: Also modifies masked-window fast-copy shadow-slot handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: skipping redundant shadow-slot clears in region fast copies.
Description check ✅ Passed The description covers the motivation, fix, effect, correctness, and validation, so it is mostly complete despite not matching the template exactly.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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/6794-b-shadow-clear-dedup

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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@test-files/test_gap_region_shadow_clear_gc.ts`:
- Around line 8-20: Restore encipher to the complete bcryptjs-style 16-round,
over-130-statement shape so it remains non-inlined with its shadow frame live,
and move the deliberate junk allocations into encipher while it is executing.
Update the regression test’s GC trigger accordingly, then validate it using the
repository parity-test scripts rather than a direct incompatible Node runtime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f19e391-280f-456c-8a55-69fc4a7f243a

📥 Commits

Reviewing files that changed from the base of the PR and between 19b2b11 and 110f967.

📒 Files selected for processing (9)
  • changelog.d/6846-region-shadow-clear-dedup.md
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/shadow_slot.rs
  • crates/perry-codegen/src/stmt/masked_window_region.rs
  • test-files/test_gap_region_shadow_clear_gc.ts

Comment thread test-files/test_gap_region_shadow_clear_gc.ts Outdated
…w-clear GC test

Addresses CodeRabbit on #6846: extend encipher to the full 16-round bcryptjs
shape so it is robustly non-inlined (keeps its own shadow frame), and correct the
comments — a region-versioned function is alloc-free/call-free by construction, so
a GC cannot fire inside its fast copy; the skip's safety is static (suppression
keeps the slot 0). The test guards the broader region+GC interaction under real
and forced (PERRY_GC_FORCE_EVACUATE) collections, byte-identical to Node via the
parity harness.
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Addressed in 32e5efe:

  • Full shape / non-inlining: encipher is now the full 16-round bcryptjs
    shape and is verified non-inlined (its define carries no alwaysinline, so it
    keeps its own shadow frame and the region versioner runs on the whole round
    chain). The overclaiming comments ("130+ statements", "frames live") are
    corrected.

  • "Allocate during encipher execution" — not feasible, by design. A
    region-versioned function is alloc-free and call-free by construction (the
    region matcher rejects any statement that calls/allocates/stores — that is what
    makes the body region-matchable). So a GC can never fire inside encipher's
    fast copy, and moving the junk allocations into it would simply disqualify the
    region and stop exercising this path. The skip's safety is therefore static,
    not dynamic: emit_shadow_slot_update_for_expr suppresses every write to a
    masked_region_scalar_locals member, so a slot that was cleared to 0 provably
    stays 0 for the rest of the copy — the skipped clear re-zeros an already-zero
    slot. No runtime state can exist in which it drops a live root.

  • The test now guards the realistic interaction it can: many enter/leave cycles of
    a region function under real and forced (PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1) collections, byte-identical to Node through the
    parity gap harness.

@proggeramlug
proggeramlug merged commit 551de05 into main Jul 26, 2026
28 of 30 checks passed
@proggeramlug
proggeramlug deleted the perf/6794-b-shadow-clear-dedup branch July 26, 2026 10:31
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