You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A design proposal, not a patch. It comes out of #9253, but the pattern it describes is why that issue needed "a new loop tier" instead of reusing three existing ones.
The defect, stated once
Codegen re-proves object properties at every use, because there is no way to express "this receiver is validated, and stays validated until here." A moving collector can relocate at any safepoint, so each use conservatively re-derives the lot: reload from the shadow slot, re-check the NaN-box tag, re-dereference the header, re-test the integrity flags, re-read length and capacity.
16_matrix_multiply is the clean specimen (#9253). 97.3% of its time is inside the generated function — no runtime calls at all — and perf annotate puts the hot instructions here:
vmulsd/vaddsd are nowhere near the top. The receivers are loop-invariant parameters, and their guard is re-run 16.7M times. The IR agrees: 27 shadow-root stores and 5 atomic barrier-count loads to accomplish 2 load doubles.
Why we keep solving it privately
Every tier has answered this question on its own terms:
table
fact
scope
invalidation
cached_lengths
array length
one loop
manual remove
bounded_index_pairs
(arr, idx) in range
scope_id
scope pop
buffer_view_slots
view + pointer state
region
hazard reasons
packed_f64_loop_facts
packed layout
scope_id + guard_id
side exit
masked_window_array_facts
window proven
scope_id
scope pop
packed_receiver_box/handle_slots + refresh
receiver handle
clone body
after each poll
Six tables, six invalidation disciplines, no shared vocabulary. The consequences are concrete:
The tier matrix multiplies. Admission is decided per (tier × receiver kind × index shape × bound shape). matmul fails on three axes at once, so it fits nothing.
LLVM cannot help. The header reload and the atomic barrier read are motion barriers, so LICM and SCEV cannot hoist what is plainly invariant. We hand-roll hoisting per tier instead.
The proposal
Make "validated receiver, scoped to a safepoint-delimited region" a first-class concept.
Region analysis. Partition each body into regions delimited by everything that can collect — GC polls, allocations, non-leaf calls — and by unwind edges. Nothing moves within a region. Seeds already exist: loop_may_allocate, the gc-leaf-function annotations, and the poll-emission logic.
Receiver descriptors. For a reference used more than once in a region, materialise once at region entry: base handle, header-derived facts (type, flags, length, capacity), validity bit. Uses consume the descriptor rather than re-deriving it.
Per-use residue is the genuinely variant part, idx u< len, which is unavoidable per element.
The six tables collapse into queries against one descriptor table with one invalidation rule.
Sequencing — deliberately not a rewrite
Phase 1 — model + equivalence lint, emitting nothing. Build the region/descriptor model and assert that wherever an existing tier claims a fact, the model agrees. This validates the abstraction against six working implementations before anything depends on it, and it is revertible by deletion.
Phase 3 — ordinary counted loops. Where matmul's win lands, with no new tier.
Phase 4 — retire the remaining tables one at a time, each removal gated by the Phase 1 lint.
Risks, stated plainly
Conservatism about what can collect is a correctness property, not a tuning knob. A region that wrongly excludes a collection point is a GC bug, not a slow benchmark. Phase 1 exists to test that judgement against six existing implementations before any code depends on it.
Derived pointers. A cached element address is invalid after a move. Descriptors should cache base handles and recompute interiors, unless the interior is registered in the stack map.
The equivalence lint is the load-bearing artifact. If it cannot be made to agree with the existing tiers, that is the signal to stop — the abstraction would be wrong, and finding that out in Phase 1 costs nothing.
Prior art
Speculative assumptions with dependency-based invalidation (V8 maps, Graal assumptions) solve the same problem in a JIT; the AOT-with-moving-GC analogue is HotSpot's GC-safe regions and derived-pointer handling in stack maps. What is proposed here is narrower than either: no speculation and no deoptimisation, only "prove once per region, refresh at the boundary."
Happy to take Phase 1 if the direction is agreed. It touches GC correctness across the compiler, so it wants a maintainer's agreement on the region-formation rule before code, not after.
A design proposal, not a patch. It comes out of #9253, but the pattern it describes is why that issue needed "a new loop tier" instead of reusing three existing ones.
The defect, stated once
Codegen re-proves object properties at every use, because there is no way to express "this receiver is validated, and stays validated until here." A moving collector can relocate at any safepoint, so each use conservatively re-derives the lot: reload from the shadow slot, re-check the NaN-box tag, re-dereference the header, re-test the integrity flags, re-read length and capacity.
16_matrix_multiplyis the clean specimen (#9253). 97.3% of its time is inside the generated function — no runtime calls at all — andperf annotateputs the hot instructions here:vmulsd/vaddsdare nowhere near the top. The receivers are loop-invariant parameters, and their guard is re-run 16.7M times. The IR agrees: 27 shadow-root stores and 5 atomic barrier-count loads to accomplish 2load doubles.Why we keep solving it privately
Every tier has answered this question on its own terms:
cached_lengthsbounded_index_pairs(arr, idx)in rangescope_idbuffer_view_slotspacked_f64_loop_factsscope_id+guard_idmasked_window_array_factsscope_idpacked_receiver_box/handle_slots+refreshSix tables, six invalidation disciplines, no shared vocabulary. The consequences are concrete:
matmulfails on three axes at once, so it fits nothing.is_denseover-restriction in perf(codegen): the inline array-store tier covers tagged receivers (prime_sieve 4.5× → 1.8× node) #9250, the side-exit reasoning in perf(codegen): packed clones read a foreign counter inline (10_nested_loops 51 → 17 ms, Node parity) #9161. Same shape each time.The proposal
Make "validated receiver, scoped to a safepoint-delimited region" a first-class concept.
loop_may_allocate, thegc-leaf-functionannotations, and the poll-emission logic.packed_receiver_refreshgeneralised — and applying it at unwind edges is what makes perf(codegen): a throw that does not construct keeps the packed fast path (4.76 → 0.58 ns) #9185's class structurally impossible rather than caught by review.idx u< len, which is unavoidable per element.Sequencing — deliberately not a rewrite
matmul's win lands, with no new tier.Risks, stated plainly
Prior art
Speculative assumptions with dependency-based invalidation (V8 maps, Graal assumptions) solve the same problem in a JIT; the AOT-with-moving-GC analogue is HotSpot's GC-safe regions and derived-pointer handling in stack maps. What is proposed here is narrower than either: no speculation and no deoptimisation, only "prove once per region, refresh at the boundary."
Happy to take Phase 1 if the direction is agreed. It touches GC correctness across the compiler, so it wants a maintainer's agreement on the region-formation rule before code, not after.
https://claude.ai/code/session_012Ys25ni6VwDKE71o1NTYAT