Skip to content

perf(codegen): gate the guarded element store's write barrier on the stored value (#7715) - #7959

Merged
proggeramlug merged 1 commit into
mainfrom
gc/7715-element-store-barrier-value-gate
Aug 12, 2026
Merged

perf(codegen): gate the guarded element store's write barrier on the stored value (#7715)#7959
proggeramlug merged 1 commit into
mainfrom
gc/7715-element-store-barrier-value-gate

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Part of #7715 (write-barrier tower, B3 "slot elision").

The measurement that chose this change

#7715 scopes B3 as "skip the write-barrier slot computation where the store
provably cannot create an old→young edge", and B4 as "carry generation in the
header". I re-profiled first, as the issue instructs, and the data pointed
somewhere neither bullet did.

Static census of js_write_barrier{,_slot} call sites across all 19 GC-corpus
programs (--trace llvm, bucketed by the block they sit in):

program gated (*.barrier, #7511/#7871) ungated idxset.recv_prop.fast value-gated wb.maybe
pipeline 20 6 6
interp 48 1 1
iso_miss 48 1 1
everything else ≤48 0 0–53

Dynamic counters (PERRY_GC_TRACE=1 PERRY_GC_DIAG=1, per-cycle write_barrier
block summed):

program calls non_pointer_child parent_not_old slow hits
pipeline 1 265 933 1 265 925 (99.999%) 0 0
churn 119 716 0 118 235 0
interp 11 539 678 322 146
iso_miss 11 302 596 172 146

pipeline's Registry.set runs this.vals[i] = v 1.44 M times. Every one of
those calls decodes the stored value, finds it is a number, and returns having
done nothing — and parent_not_old_skips is 0, so the parent-generation
gate #7511/#7871 shipped would have skipped nothing there. The lever at this
site is the VALUE test, not the generation test.

The change

js_write_barrier_slot at the two array-element store sites
(emit_guarded_inbounds_array_store, and lower_index_set_fast's JSValue
in-bounds arm) now sits behind emit_may_carry_heap_pointer_check, and then
behind emit_parent_may_need_remembering_check — the same two predicates the
class-field store has carried since #7511/#7871, nested value-first, not
fused into one and.

Nesting is load-bearing: the value test is pure register arithmetic on the
stored bits, while the parent test does a monotonic load of
@PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT that LLVM may not hoist out of
the loop. A fused and would charge that non-hoistable load to exactly the
numeric stores this exists to make free.

Soundness

Skipping on the value test alone is sound because write_barrier_slot_inner's
first action is barrier_child_prologue(child), which returns — before the SATB
shading, the armed check, the parent decode and the remembered set — when
decode_heap_addr(child) == 0. The emitted predicate is a documented
superset of that decode; gc::tests::inline_pointer_bearing_contract
enumerates the whole 16-bit tag space against it. A value carrying no heap
pointer has nothing to shade, which is why the incremental clause belongs to the
parent half and not this one; the parent half's obligations are unchanged and
stay pinned by gc::tests::inline_generation_gate_contract.

Reading the parent's header byte at arr_handle - 7 is safe on both arms: each
reached its block through a guard that already read obj_type at -8 and
gc_flags at -7, and has already performed a raw store into
arr_handle + 8 + i*8.

What deliberately stays outside the value test: the layout note. The
class-field emitter puts its note behind the same test, but that argument does
not transfer — layout_note_slot funnels
crate::array::note_element_store (#7480), and a non-pointer stored over a
pointer is exactly the store that must clear GC_ARRAY_ELEMENT_SHAPE.
js_array_note_numeric_write is likewise not skippable on a pointer test:
undefined/booleans are non-pointers that still have to downgrade the array's
raw-f64 layout.

Tests

expr/index_set_barrier_tests.rs walks the emitted def chain rather than
looking for instructions nearby. It fails on: a hard-wired br i1 true/false
that leaves the dead predicate in the block; dropping any of the three heap-tag
comparands from the value predicate; dropping either disjunct of the parent
predicate; putting the barrier on the false edge; and eliding the call instead
of guarding it.

B4 — reported as measured-negative, not attempted

Recorded in gc-handoff/BARRIER-NOTES.md §2.3/§2.5 so it is not re-derived:

  • There is no bit. gc_flags is full — gc/types.rs:975 says so in terms
    ("This is the last bit in the u8 gc_flags. Adding more flags requires
    extending GcHeader ... extending breaks ABI everywhere") — and _reserved is
    fully allocated and already obj_type-overloaded.
  • The prize is 118 k calls on one program. churn's
    parent_not_old_skips are the Old ⊊ TENURED gap: objects the non-moving
    minor promoted logically (gc/oldgen.rs) carry TENURED while still living
    in the nursery, so they pass the codegen gate and fail the runtime's
    classify_heap_generation == Old. gc/trace.rs:799-810 is explicit that this
    state must still be traced, which is why the approximation is sound and why
    closing it needs a genuinely new bit. Outside churn the whole corpus totals
    494 such calls.
  • A runtime-side !TENURED short-circuit is worth nothing either, for the
    same reason: every call that reaches barrier_parent_needs_remembering got
    there by passing the gate, so its parent is TENURED.

Measured

Both arms built from this worktree at ac52a5c38 (0.5.1490; the branch was
rebased onto b392f7b5d afterwards, and the diff is unchanged). The change is
codegen-only
(git diff --name-only touches nothing under perry-runtime/perry-stdlib),
so both link the same libperry_{runtime,stdlib}.a and the corpus was
compiled with the same output basenames in different directories.

cmp over the 19-program GC corpus: 16 identical, 3 differ — and the 3 are
exactly pipeline, interp, iso_miss, i.e. precisely the programs the census
says contain the site.
The other 16 are provably unchanged and need no timing.

Instructions retired (ab_instr.py, best of 7, dev box, spread ≤ 0.86%):

program base instr (M) fix instr (M) Δ instr Δ cycles
pipeline 2657.8 2582.0 −2.85% −3.91%
interp 11540.4 11540.7 +0.00% +0.73%
iso_miss 14101.6 14101.1 −0.00% −0.11%

(Direction only — final wall-clock belongs on the quiet mini. interp's cycles
column moves with the box; its instruction count is flat to three digits and its
binary differs by one gated site.)

Correctness

  • 19/19 corpus programs byte-identical to m0810/expected/, exit 0 — including
    the iso_miss canary (checksum 437840 misses 0).
  • 9 programs re-run byte-identical under PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800, under PERRY_GC_VERIFY_EVACUATION=1,
    and (the 3 that differ) under PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=64.
  • cargo test -p perry-codegen --lib: 921 passed, 0 failed.
  • The three new tests were sabotage-run, not merely written:
    hard-wiring the value gate's branch to true (leaving the dead predicate in
    the block) fails only the_element_store_barrier_sits_behind_a_live_value_test;
    dropping the incremental disjunct fails only the two parent-gate tests
    (including perf(codegen): interp 1.095 -> 0.844 s — gate the class-field barrier on the parent's generation, and let a hot recursive function inline its bump allocator #7871's existing one); eliding the call inside the barrier block
    fails only the_gated_element_store_still_reaches_the_barrier_call.

Summary by CodeRabbit

  • Bug Fixes

    • Improved guarded array updates by skipping write-barrier work when stored values cannot contain heap references.
    • Preserved generation and incremental-marking checks for values that may reference managed memory.
    • Maintained existing behavior for numeric and raw array stores.
  • Tests

    • Added comprehensive coverage validating barrier conditions and generated code paths.

@coderabbitai

coderabbitai Bot commented Aug 12, 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: 89eec4b4-e8d7-4e91-afe5-102fb17abc63

📥 Commits

Reviewing files that changed from the base of the PR and between 3cce02e and 6a58d34.

📒 Files selected for processing (1)
  • crates/perry-codegen/tests/typed_shape_descriptors.rs

📝 Walkthrough

Walkthrough

Guarded array-element stores now emit write barriers only when the stored value may contain a heap pointer and the parent requires generation or incremental-marking handling. LLVM IR tests validate predicate ordering and barrier emission.

Changes

Guarded array-store barrier gating

Layer / File(s) Summary
Value and generation barrier helper
crates/perry-codegen/src/expr/write_barrier.rs, crates/perry-codegen/src/expr/mod.rs
Adds a reusable barrier emitter that checks heap-pointer value tags before parent-generation and incremental-marking state.
Array-store barrier integration
crates/perry-codegen/src/expr/index_set_guarded.rs, crates/perry-codegen/src/expr/index.rs
Separates slot storage from barrier emission and preserves the stored value, array handle, and element address for conditional barrier generation.
IR barrier regression coverage
crates/perry-codegen/src/expr/class_field_barrier_tests.rs, crates/perry-codegen/src/expr/index_set_barrier_tests.rs, crates/perry-codegen/tests/typed_shape_descriptors.rs, changelog.d/7715-element-store-barrier-value-gate.md
Adds reusable IR inspection helpers, def-use and control-flow assertions, barrier-call checks, and changelog documentation.

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

Possibly related PRs

  • PerryTS/perry#7536 — Adds related value-pointer gating for class-field write barriers.
  • PerryTS/perry#7602 — Adds related generation-gated barrier handling for array stores.
  • PerryTS/perry#7858 — Uses similar control-flow and def-use validation for compiler-generated write barriers.

Suggested reviewers: thehypnoo, jdalton

Sequence Diagram(s)

sequenceDiagram
  participant GuardedArrayStore
  participant WriteBarrierEmitter
  participant RuntimeBarrier
  GuardedArrayStore->>WriteBarrierEmitter: Pass stored value and slot metadata
  WriteBarrierEmitter->>WriteBarrierEmitter: Check heap-pointer value tags
  WriteBarrierEmitter->>WriteBarrierEmitter: Check parent generation and marking state
  WriteBarrierEmitter->>RuntimeBarrier: Emit js_write_barrier_slot when checks pass
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: gating guarded element-store write barriers on the stored value.
Description check ✅ Passed The description thoroughly explains the change, rationale, measurements, soundness, tests, and related issue, despite not using every template heading.
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 gc/7715-element-store-barrier-value-gate

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

🧹 Nitpick comments (1)
crates/perry-codegen/src/expr/index_set_barrier_tests.rs (1)

79-90: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the local-receiver in-bounds path.

This fixture intentionally bypasses lower_index_set_fast, but crates/perry-codegen/src/expr/index.rs also changed its deferred barrier integration. Add a sibling IR fixture with a stack-local array receiver. Assert its idxset.inbounds.barrier.maybe path reaches the value gate and barrier call.

🤖 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/src/expr/index_set_barrier_tests.rs` around lines 79 -
90, Add a sibling IR fixture in index_set_barrier_tests.rs using a stack-local
array receiver, while preserving the integer-literal-seeded for-loop index and
Any value setup. Assert that the idxset.inbounds.barrier.maybe path reaches the
value gate and invokes the deferred barrier call, covering the local-receiver
integration in index.rs.
🤖 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 `@crates/perry-codegen/src/expr/index_set_barrier_tests.rs`:
- Around line 241-256: Extend the assertions in the index-set barrier test to
verify the final value-gate OR includes the raw-address AND predicate from
emit_may_carry_heap_pointer_check. Also assert that this raw-address branch
compares the extracted top-16 tag against zero and the value bits against the
4096 floor, preserving coverage for untagged heap addresses alongside the three
tagged forms.

---

Nitpick comments:
In `@crates/perry-codegen/src/expr/index_set_barrier_tests.rs`:
- Around line 79-90: Add a sibling IR fixture in index_set_barrier_tests.rs
using a stack-local array receiver, while preserving the integer-literal-seeded
for-loop index and Any value setup. Assert that the
idxset.inbounds.barrier.maybe path reaches the value gate and invokes the
deferred barrier call, covering the local-receiver integration in index.rs.
🪄 Autofix

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: a27a88e0-f02e-41f0-98cb-9250502a7bdf

📥 Commits

Reviewing files that changed from the base of the PR and between b392f7b and 3cce02e.

📒 Files selected for processing (7)
  • changelog.d/7715-element-store-barrier-value-gate.md
  • crates/perry-codegen/src/expr/class_field_barrier_tests.rs
  • crates/perry-codegen/src/expr/index.rs
  • crates/perry-codegen/src/expr/index_set_barrier_tests.rs
  • crates/perry-codegen/src/expr/index_set_guarded.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/write_barrier.rs

Comment on lines +241 to +256
// Every heap tag the runtime's `decode_heap_addr` accepts must be compared
// against. Dropping one is the direction that STRANDS a child, so the whole
// comparand set is pinned, not just its shape.
for tag in ["32765", "32767", "32762"] {
assert!(
body.contains(&format!(", {tag}\n")) || body.contains(&format!(", {tag}")),
"the value gate never compares against tag {tag}; a value carrying \
it would skip the barrier while the runtime would have decoded a \
heap address from it:\n{body}"
);
}
assert!(
body.contains("lshr i64") && body.contains(", 48"),
"the value gate does not derive the top 16 bits of the stored value, \
so it is testing something other than the NaN-box tag:\n{body}"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the raw heap-address predicate.

emit_may_carry_heap_pointer_check also accepts an untagged raw heap address when top16 == 0 and value_bits >= 4096. This test checks only the three tagged forms. A regression that removes is_raw_addr will pass this test but skip a required barrier for that value form.

Assert that the final value-gate or includes the raw-address and, then assert its zero-tag and floor comparisons.

🤖 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/src/expr/index_set_barrier_tests.rs` around lines 241 -
256, Extend the assertions in the index-set barrier test to verify the final
value-gate OR includes the raw-address AND predicate from
emit_may_carry_heap_pointer_check. Also assert that this raw-address branch
compares the extracted top-16 tag against zero and the value bits against the
4096 floor, preserving coverage for untagged heap addresses alongside the three
tagged forms.

@proggeramlug
proggeramlug force-pushed the gc/7715-element-store-barrier-value-gate branch from 3cce02e to 6a58d34 Compare August 12, 2026 12:25
@proggeramlug

Copy link
Copy Markdown
Contributor Author

★ Follow-up push: cargo test -p perry-codegen --lib was 921/921 green while
cargo test -p perry-codegen (all targets) was red
typed_shape_descriptors::pointer_store_into_numeric_array_keeps_layout_note_and_barrier
asserted js_write_barrier_slot appeared between the idxset.inbounds. and
idxset.check_cap. labels, which is exactly where the barrier stopped being.
Integration suites under crates/*/tests/*.rs do not run per-PR, so that would
have landed green and sat red until the nightly.

Updated, not weakened: the assertion now follows the edge — the in-bounds
arm must branch into idxset.inbounds.barrier.maybe., and the gate block must
still contain the call and the incremental-count read. "The call exists
somewhere in the module" would pass even if this arm stopped reaching a barrier.

That test is also the standing proof that lower_index_set_fast's JSValue arm
is reachable in real programs — a pointer stored into a statically number[]
local — even though the 19-program GC corpus contains zero such sites.

Full cargo test -p perry-codegen: exit 0, 27/27 binaries ok. Compiler output
is unchanged by this push (test-only), so the measurements above still stand.

@proggeramlug
proggeramlug merged commit d78efca into main Aug 12, 2026
1 of 19 checks passed
@proggeramlug
proggeramlug deleted the gc/7715-element-store-barrier-value-gate branch August 12, 2026 12:29
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