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
Generalize the guarded object-write optimizations landed by #6811 beyond the
single #6759 acceptance-loop shape, without weakening JavaScript semantics or
GC safety.
#6811 completed #6809 and beat Node on the canonical write micro, but its
largest win is intentionally narrow. This issue tracks the remaining coverage
work: identify which real object writes still miss the static write PIC or the
whole-loop clone, rank those misses with measurements, and extend the sound
fast paths in small reviewable slices.
Start from current origin/main. The starting point must contain merge commit 20f19758f370f7616530824218d87f46a67cd3f0 ("perf: beat Node on object-write
micro (#6809) (#6811)"). Do not resume from a pre-#6811 worktree.
a constant counted outer for loop containing exactly one constant
counted inner for loop;
inner start at zero and a finite constant dense prefix;
one immutable const o = objects[i] alias;
exactly two static existing-field writes;
numeric expressions composed only from loop counters, numeric
constants, +, and -;
no labels, calls, allocations, safepoints, or other body statements.
The runtime scans the full receiver prefix once and proves regular
objects, dense elements, one shared keys array, writable own slots,
descriptor/frozen/sealed/no-extend state, and typed raw-f64 layout where
applicable. Failure enters an untouched semantic clone before any store.
Canonical acceptance micro
Keep this exact source for historical comparability:
Do not set PERRY_NO_AUTO_OPTIMIZE. Do not override release
codegen-units; the repository's release/LTO profile is part of the
measurement.
Alternate Node and Perry runs on the same idle machine. Record versions,
host/architecture, all raw samples, and the median of at least 15 runs.
Keep the checksum identical. For new matrix cells, also provide a scaled
version lasting at least about 100 ms to reduce Date.now() quantization.
Never report a gain from compile-only IR evidence. Measure the produced
executable and verify output parity first.
Required first step: a coverage/rejection matrix
Before extending code, benchmark and classify at least these cells. Record
whether each hits the generic runtime, the static PIC, or a loop clone, and the
reason an expected fast path rejects.
numeric scalar, pointer-capable value, allocating literal, function call
Receiver shapes
monomorphic, 2-shape, 4-shape, shape transition before loop
Fields per iteration
1, 2, 4, 8
Loop form
single counted loop, current nested loop, stable local bounds, non-zero start
Storage
inline existing slot, wide/overflow object
Receiver kind
anonymous object, class instance, class-id-zero plain object
Add lightweight deterministic observability for compile-time rejection reasons
if needed. It must be disabled by default and must not add work to production
hit paths. A test-only classifier or compiler artifact is preferable to
runtime logging in the hot loop.
Post the completed matrix and flame/sample evidence to this issue before
choosing the first generalization. Optimize the highest-volume sound miss, not
the most convenient matcher case.
Candidate implementation slices
These are ordered by likely leverage, but measurements may reorder them.
A. Root the evaluated PutValue reference across safepointing RHS expressions
The static PIC currently rejects allocating/calling RHS expressions because
JavaScript evaluates the target/key reference before the RHS, and GC could
move an unrooted target held only in SSA. Introduce an explicit, correctly
scoped root/reference temporary so target and key survive RHS evaluation
without being re-evaluated. Then extend the PIC only where evaluation order,
exceptions, and write result semantics remain identical.
Required proof:
target, key, and RHS are evaluated exactly once and in spec order;
thrown RHS leaves no store;
target movement under forced moving GC is handled;
pointer-capable values take the layout-note/string-alias/write-barrier path;
no root leaks across loop backedges or function exits.
B. Generalize the numeric whole-loop proof
Replace the hard-coded "exactly two fields" representation with a bounded
descriptor for N static numeric own slots. Candidate initial bound: 1–4
fields, justified by code size and benchmark data. Extend one loop-shape
dimension at a time (for example, a single counted loop before variable
bounds).
The successful clone must remain finite and completely call/GC/safepoint-free
while it holds raw array/object pointers. If a proposed expression can call,
allocate, throw, invoke coercion, or observe user code, it does not belong in
this clone without a different replay/rooting design.
Avoid allocating a descriptor or interning keys per iteration. The preflight
may do bounded work once; the fast body should contain only receiver loads,
numeric operations, and direct stores.
C. Stable dynamic-key PIC
If the matrix shows o[k] is the dominant remaining gap, add a cache that
also validates key identity. Start with an already-interned, loop-invariant
string key. Do not put the global intern-table mutex or UTF-8/key conversion
back on the per-hit path.
Any cache design must distinguish (shape token, key, slot), preserve symbol
and non-string PropertyKey semantics via fallback, and remain correct across
shape transitions and GC movement.
D. Small polymorphic write PIC
If 2–4 stable shapes dominate real misses, consider a bounded polymorphic
cache. Keep code size bounded and measure branch/I-cache costs against the
monomorphic PIC. Do not replace the never-reused/discriminated ShapeId token
with a raw address token susceptible to reuse/ABA.
Load-bearing correctness invariants
Do not weaken these to make a benchmark hit:
Preserve full strict/sloppy [[Set]] behavior and the original fallback.
Preserve target → key → RHS evaluation order, side effects, exceptions, and
assignment result.
Recheck mutable receiver state on every PIC hit: frozen, sealed,
non-extensible, descriptors, typed-layout intact, forwarded state, receiver
kind, and slot bounds.
Accessors, inherited setters, proxies, handles, native modules, class
objects, arrays/typed arrays, primitive receivers, and target/receiver
mismatches must retain their semantic paths unless separately proven.
Use the read/write PIC's discriminated, never-reused ShapeId token rules.
Shared keys pointers are valid only under their existing lifetime proof.
INLINE_SLOT_FLOOR is 4, not 8. Never over-read small object
allocations.
Pointer-capable direct stores must continue through emit_jsvalue_slot_store_scalar_aware_on_block or an equivalently complete
layout-note/string-alias/write-barrier implementation.
Raw numeric stores may omit barriers only when the compiler and runtime
jointly prove pointer-free storage/value semantics.
A whole-loop clone may retain raw pointers only while it contains no call,
allocation, safepoint, barrier helper, or side exit.
Persistent shadow-root barrier activity is an atomic runtime global. Keep
the sequentially-consistent LLVM atomic load introduced by perf: beat Node on object-write micro (#6809) #6811; volatile
is not a substitute for atomic access.
Every new raw generated heap/global store needs a justified GC_STORE_AUDIT(...) marker and must pass scripts/gc_store_site_inventory.py.
Typed-feedback/verification/diagnostic modes and explicit inline-fast-path
disable gates must force the appropriate semantic path.
Correctness matrix
Every optimized slice needs Node/Perry output parity and focused regression
coverage for:
own writable data fields;
readonly/non-writable own fields in strict and sloppy code;
own and inherited accessors/setters;
frozen, sealed, and non-extensible receivers;
delete/re-add and shape transition before/during the loop;
mixed shapes, holes, short arrays, duplicate target keys, and bounds edges;
proxies, handles, native modules, class objects, and receiver != target;
pointer values, young-to-old edges, incremental marking, and forced moving-GC
rewrite/evacuation;
typed-layout intact and downgraded objects;
array-index/prototype descriptor invalidation;
allocating/throwing RHS and observable target/key/RHS evaluation order;
fast-path disable, typed-feedback, and verification environment modes.
Keep the semantic fallback in the generated-IR tests. A test that only proves
the fast block exists is insufficient.
Acceptance criteria
Baseline matrix and rejection reasons are posted before implementation.
Each newly supported matrix cell has:
- a paired eligible and forced-fallback test;
- generated-IR evidence for the intended guard/hit/miss structure;
- executable Node/Perry parity;
- before/after alternating-run measurements.
Call-free monomorphic existing-field loop cells target Node parity or
better. Other generalized cells must show a meaningful measured win
without slowing established paths; do not claim "across the board"
without matrix evidence.
Numeric whole-loop hit bodies contain no calls, allocations, barriers,
coercions, or side exits.
Pointer-capable PIC hits retain complete GC/layout/string-alias handling.
No new untriaged gap-suite failure, compile failure, or crash.
Fast-path code-size growth is measured and bounded.
Also compile and execute the benchmark/adversarial corpus with the release
compiler/runtime/static archives. CI must pass clippy, compiler-output
regression, Windows, and the conformance shards.
This issue is complete when the supported object-write coverage is documented
by the matrix, the highest-value safe gaps have landed, and the remaining
fallback categories are explicitly justified by semantics or measured lack of
benefit.
Objective
Generalize the guarded object-write optimizations landed by #6811 beyond the
single #6759 acceptance-loop shape, without weakening JavaScript semantics or
GC safety.
#6811 completed #6809 and beat Node on the canonical write micro, but its
largest win is intentionally narrow. This issue tracks the remaining coverage
work: identify which real object writes still miss the static write PIC or the
whole-loop clone, rank those misses with measurements, and extend the sound
fast paths in small reviewable slices.
Start from current
origin/main. The starting point must contain merge commit20f19758f370f7616530824218d87f46a67cd3f0("perf: beat Node on object-writemicro (#6809) (#6811)"). Do not resume from a pre-#6811 worktree.
What #6811 established
Measured on the same macOS/ARM64 machine using 15 alternating runs:
The result is real, but it is not a blanket optimization for every
object[key] = value:Broad runtime improvements
user-descriptor gates.
Guarded static-key write PIC
crates/perry-codegen/src/expr/proxy_reflect.rs(
lower_put_value_static_write_ic)crates/perry-runtime/src/proxy/put_value.rs(
js_put_value_set_ic_miss)guards to pass.
Whole-loop numeric object-array clone
crates/perry-codegen/src/stmt/loops.rs(
match_object_array_write2_loop,lower_object_array_write2_versioned_for)crates/perry-runtime/src/proxy/put_value.rs(
js_object_array_numeric_write2_guard)forloop containing exactly one constantcounted inner
forloop;const o = objects[i]alias;constants,
+, and-;objects, dense elements, one shared keys array, writable own slots,
descriptor/frozen/sealed/no-extend state, and typed raw-f64 layout where
applicable. Failure enters an untouched semantic clone before any store.
Canonical acceptance micro
Keep this exact source for historical comparability:
Measurement protocol
Use a release build from a clean worktree based on current
origin/main.Use an isolated Cargo target directory for that worktree:
PERRY_OBJECT_WRITE_TARGET=/absolute/path/to/an/isolated/cargo-target CARGO_TARGET_DIR="$PERRY_OBJECT_WRITE_TARGET" \ cargo build --release \ -p perry -p perry-runtime-static -p perry-stdlib-staticCompile with the default auto-optimizing pipeline:
Do not set
PERRY_NO_AUTO_OPTIMIZE. Do not override releasecodegen-units; the repository's release/LTO profile is part of the
measurement.
Alternate Node and Perry runs on the same idle machine. Record versions,
host/architecture, all raw samples, and the median of at least 15 runs.
Keep the checksum identical. For new matrix cells, also provide a scaled
version lasting at least about 100 ms to reduce
Date.now()quantization.Never report a gain from compile-only IR evidence. Measure the produced
executable and verify output parity first.
Required first step: a coverage/rejection matrix
Before extending code, benchmark and classify at least these cells. Record
whether each hits the generic runtime, the static PIC, or a loop clone, and the
reason an expected fast path rejects.
o.x,o["x"], stableo[k], alternating dynamic keysAdd lightweight deterministic observability for compile-time rejection reasons
if needed. It must be disabled by default and must not add work to production
hit paths. A test-only classifier or compiler artifact is preferable to
runtime logging in the hot loop.
Post the completed matrix and flame/sample evidence to this issue before
choosing the first generalization. Optimize the highest-volume sound miss, not
the most convenient matcher case.
Candidate implementation slices
These are ordered by likely leverage, but measurements may reorder them.
A. Root the evaluated PutValue reference across safepointing RHS expressions
The static PIC currently rejects allocating/calling RHS expressions because
JavaScript evaluates the target/key reference before the RHS, and GC could
move an unrooted target held only in SSA. Introduce an explicit, correctly
scoped root/reference temporary so target and key survive RHS evaluation
without being re-evaluated. Then extend the PIC only where evaluation order,
exceptions, and write result semantics remain identical.
Required proof:
B. Generalize the numeric whole-loop proof
Replace the hard-coded "exactly two fields" representation with a bounded
descriptor for
Nstatic numeric own slots. Candidate initial bound: 1–4fields, justified by code size and benchmark data. Extend one loop-shape
dimension at a time (for example, a single counted loop before variable
bounds).
The successful clone must remain finite and completely call/GC/safepoint-free
while it holds raw array/object pointers. If a proposed expression can call,
allocate, throw, invoke coercion, or observe user code, it does not belong in
this clone without a different replay/rooting design.
Avoid allocating a descriptor or interning keys per iteration. The preflight
may do bounded work once; the fast body should contain only receiver loads,
numeric operations, and direct stores.
C. Stable dynamic-key PIC
If the matrix shows
o[k]is the dominant remaining gap, add a cache thatalso validates key identity. Start with an already-interned, loop-invariant
string key. Do not put the global intern-table mutex or UTF-8/key conversion
back on the per-hit path.
Any cache design must distinguish
(shape token, key, slot), preserve symboland non-string PropertyKey semantics via fallback, and remain correct across
shape transitions and GC movement.
D. Small polymorphic write PIC
If 2–4 stable shapes dominate real misses, consider a bounded polymorphic
cache. Keep code size bounded and measure branch/I-cache costs against the
monomorphic PIC. Do not replace the never-reused/discriminated ShapeId token
with a raw address token susceptible to reuse/ABA.
Load-bearing correctness invariants
Do not weaken these to make a benchmark hit:
[[Set]]behavior and the original fallback.assignment result.
non-extensible, descriptors, typed-layout intact, forwarded state, receiver
kind, and slot bounds.
objects, arrays/typed arrays, primitive receivers, and target/receiver
mismatches must retain their semantic paths unless separately proven.
Shared keys pointers are valid only under their existing lifetime proof.
INLINE_SLOT_FLOORis 4, not 8. Never over-read small objectallocations.
emit_jsvalue_slot_store_scalar_aware_on_blockor an equivalently completelayout-note/string-alias/write-barrier implementation.
jointly prove pointer-free storage/value semantics.
allocation, safepoint, barrier helper, or side exit.
the sequentially-consistent LLVM atomic load introduced by perf: beat Node on object-write micro (#6809) #6811; volatile
is not a substitute for atomic access.
GC_STORE_AUDIT(...)marker and must passscripts/gc_store_site_inventory.py.disable gates must force the appropriate semantic path.
Correctness matrix
Every optimized slice needs Node/Perry output parity and focused regression
coverage for:
rewrite/evacuation;
Keep the semantic fallback in the generated-IR tests. A test that only proves
the fast block exists is insufficient.
Acceptance criteria
regression (target: within 10% on the same machine/protocol).
- a paired eligible and forced-fallback test;
- generated-IR evidence for the intended guard/hit/miss structure;
- executable Node/Perry parity;
- before/after alternating-run measurements.
better. Other generalized cells must show a meaningful measured win
without slowing established paths; do not claim "across the board"
without matrix evidence.
coercions, or side exits.
Validation gates
At minimum for every implementation PR:
Also compile and execute the benchmark/adversarial corpus with the release
compiler/runtime/static archives. CI must pass clippy, compiler-output
regression, Windows, and the conformance shards.
Existing tests to extend
crates/perry-codegen/tests/native_proof_regressions.rsstatic_put_value_uses_write_pic_for_call_free_rhsstatic_put_value_rejects_write_pic_when_rhs_can_allocatenested_same_shape_object_writes_version_the_whole_loopcrates/perry-codegen/tests/shadow_slot_hygiene.rsimmutable_index_alias_binds_once_but_keeps_incremental_root_barriercrates/perry-runtime/src/proxy.rsobject_array_numeric_write2_guard_requires_complete_uniform_proofcrates/perry-runtime/src/gc/tests/barrier.rsSession-start checklist
origin/main; confirmit contains
20f19758f.process is still running.
If it does not reproduce, diagnose the baseline first.
fragment plus benchmark/test evidence.
Related work
and the atomic shadow-root guard fix
This issue is complete when the supported object-write coverage is documented
by the matrix, the highest-value safe gaps have landed, and the remaining
fallback categories are explicitly justified by semantics or measured lack of
benefit.