Summary
#6812's "slice A" — root the evaluated PutValue reference so the static write
PIC can accept an RHS that allocates, calls, or otherwise reaches a safepoint —
was specified in that issue's body and never landed. It is now the largest
remaining family in the object-write matrix after class_id == 0 receivers
(#8098), covering three cells at once.
Measured on origin/main @ 514ff7c9a (v0.5.1510) against Node 26.5.1, using
the committed harness benchmarks/object-write-6812/run_matrix.py --samples 3
(it verifies (writes, sink) on every Node/Perry pair and aborts on mismatch):
| cell |
node |
perry |
P/node |
2026-07-23 baseline |
rhs_call — RHS is a function call |
513 ms |
9,376 ms |
18.28 |
26.9x |
rhs_pointer — pointer-capable RHS |
635 ms |
5,848 ms |
9.21 |
12.3x |
rhs_allocating — RHS allocates ("s" + i) |
548 ms |
2,540 ms |
4.64 |
93.5x |
For contrast, the same matrix's rhs_numeric cell — identical in every respect
except that the RHS is a scalar — runs at 0.78x (Perry 459 ms vs Node
585 ms). The entire difference is the safepoint-free rule.
Full 26-cell table:
#6812 (comment)
The gate
crates/perry-codegen/src/expr/proxy_reflect.rs:441-448, unchanged since #6811:
// The assignment reference (target + static key) is evaluated before the
// RHS. Until PutValue reference temporaries have dedicated GC roots, an
// allocating/calling RHS could move the already-evaluated target while its
// SSA value remains stale. Keep the inline PIC to call-free expressions;
// the existing runtime lowering handles every other RHS.
if !put_value_rhs_is_safepoint_free(ctx, value) {
return Ok(None);
}
put_value_rhs_is_safepoint_free (:1029) admits only locals, literals, and
numeric Binary over them. crates/perry-runtime/src/proxy/put_value.rs:465
separately records a "discarded safepointing-RHS approach", noting that the
dynamic-key IC's hot path deliberately "roots or revalidates" nothing.
Why it may be tractable now
The stated blocker is "until PutValue reference temporaries have dedicated GC
roots". That predates the Layer-1 rooting-by-construction migration (#7615),
which moved perry-codegen's lowerings onto an API built for exactly this shape.
The right first step is to check whether the reference temporary can now be
expressed as a rooted construction rather than a bare SSA value, before
designing anything new.
Correctness bar (from #6812's body, unchanged)
- target, key, and RHS evaluated exactly once, in spec order;
- a throwing RHS leaves no store;
- target movement under forced moving GC handled
(PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1, and PERRY_GEN_GC=0);
- pointer-capable values keep the full layout-note / string-alias / write-barrier
path (emit_jsvalue_slot_store_scalar_aware_on_block or an equivalently
complete implementation);
- no root leaks across loop back-edges or function exits;
- paired eligible and forced-fallback tests, plus generated-IR evidence for the
intended guard/hit/miss structure — a test that only proves the fast block
exists is insufficient.
Acceptance
rhs_call, rhs_pointer and rhs_allocating show a measured improvement
under run_matrix.py with 15 samples on an idle host; rhs_numeric's 0.78x
and the other 13 currently-beating cells do not regress.
- Identical
(writes, sink) on all 26 cells.
scripts/gc_root_dominance_check.py over
scripts/gc_root_dominance_corpus.sh stays at 0 violations with an empty
allowlist.
Split out of #6812. Sibling: #8098 (class_id == 0 receivers, 78x).
Summary
#6812's "slice A" — root the evaluated PutValue reference so the static write
PIC can accept an RHS that allocates, calls, or otherwise reaches a safepoint —
was specified in that issue's body and never landed. It is now the largest
remaining family in the object-write matrix after
class_id == 0receivers(#8098), covering three cells at once.
Measured on
origin/main@514ff7c9a(v0.5.1510) against Node 26.5.1, usingthe committed harness
benchmarks/object-write-6812/run_matrix.py --samples 3(it verifies
(writes, sink)on every Node/Perry pair and aborts on mismatch):rhs_call— RHS is a function callrhs_pointer— pointer-capable RHSrhs_allocating— RHS allocates ("s" + i)For contrast, the same matrix's
rhs_numericcell — identical in every respectexcept that the RHS is a scalar — runs at 0.78x (Perry 459 ms vs Node
585 ms). The entire difference is the safepoint-free rule.
Full 26-cell table:
#6812 (comment)
The gate
crates/perry-codegen/src/expr/proxy_reflect.rs:441-448, unchanged since #6811:put_value_rhs_is_safepoint_free(:1029) admits only locals, literals, andnumeric
Binaryover them.crates/perry-runtime/src/proxy/put_value.rs:465separately records a "discarded safepointing-RHS approach", noting that the
dynamic-key IC's hot path deliberately "roots or revalidates" nothing.
Why it may be tractable now
The stated blocker is "until PutValue reference temporaries have dedicated GC
roots". That predates the Layer-1 rooting-by-construction migration (#7615),
which moved perry-codegen's lowerings onto an API built for exactly this shape.
The right first step is to check whether the reference temporary can now be
expressed as a rooted construction rather than a bare SSA value, before
designing anything new.
Correctness bar (from #6812's body, unchanged)
(
PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1, andPERRY_GEN_GC=0);path (
emit_jsvalue_slot_store_scalar_aware_on_blockor an equivalentlycomplete implementation);
intended guard/hit/miss structure — a test that only proves the fast block
exists is insufficient.
Acceptance
rhs_call,rhs_pointerandrhs_allocatingshow a measured improvementunder
run_matrix.pywith 15 samples on an idle host;rhs_numeric's 0.78xand the other 13 currently-beating cells do not regress.
(writes, sink)on all 26 cells.scripts/gc_root_dominance_check.pyoverscripts/gc_root_dominance_corpus.shstays at 0 violations with an emptyallowlist.
Split out of #6812. Sibling: #8098 (
class_id == 0receivers, 78x).