Skip to content

perf: generalize guarded object-write fast paths beyond the #6811 two-field loop #6812

Closed

Description

@proggeramlug

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 commit
20f19758f370f7616530824218d87f46a67cd3f0 ("perf: beat Node on object-write
micro (#6809) (#6811)"). Do not resume from a pre-#6811 worktree.

What #6811 established

Measured on the same macOS/ARM64 machine using 15 alternating runs:

implementation median for 9.6M existing-field writes
Node.js 7 ms
Perry before #6809 about 16 s
Perry after runtime + per-site PIC work, before loop versioning 39–40 ms
Perry after #6811 5 ms

The result is real, but it is not a blanket optimization for every
object[key] = value:

  1. Broad runtime improvements

    • Header-first receiver classification.
    • Existing-own-data overwrite routing.
    • Builtin descriptor installs no longer unnecessarily poison process-wide
      user-descriptor gates.
    • Correct prototype/descriptor/array invalidation handling.
  2. Guarded static-key write PIC

    • crates/perry-codegen/src/expr/proxy_reflect.rs
      (lower_put_value_static_write_ic)
    • Runtime miss/priming:
      crates/perry-runtime/src/proxy/put_value.rs
      (js_put_value_set_ic_miss)
    • Currently requires:
      • a literal string key;
      • target and receiver to be the same expression;
      • a safepoint-free/call-free RHS;
      • an eligible regular existing-own-data receiver;
      • one matching shape token (monomorphic cache);
      • inline, in-bounds storage and all mutable descriptor/frozen/typed-layout
        guards to pass.
  3. Whole-loop numeric object-array clone

    • Matcher/emitter:
      crates/perry-codegen/src/stmt/loops.rs
      (match_object_array_write2_loop,
      lower_object_array_write2_versioned_for)
    • Runtime preflight:
      crates/perry-runtime/src/proxy/put_value.rs
      (js_object_array_numeric_write2_guard)
    • Currently recognizes only:
      • 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:

// #6759/#6809 acceptance: property writes to existing fields.
const objs: any[] = [];
for (let i = 0; i < 2400; i++) {
  objs.push({ a: i, b: i * 2, c: 0, d: 0 });
}

const t0 = Date.now();
for (let r = 0; r < 2000; r++) {
  for (let i = 0; i < 2400; i++) {
    const o = objs[i];
    o.c = r + i;
    o.d = r - i;
  }
}
const t1 = Date.now();

let sink = 0;
for (let i = 0; i < 2400; i++) {
  sink += objs[i].c + objs[i].d;
}
console.log("write_ms", t1 - t0, "sink", sink);

Measurement protocol

  1. 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-static
  2. Compile with the default auto-optimizing pipeline:

    PERRY_RUNTIME_DIR="$PERRY_OBJECT_WRITE_TARGET/release" \
      "$PERRY_OBJECT_WRITE_TARGET/release/perry" write.ts -o bin_write

    Do not set PERRY_NO_AUTO_OPTIMIZE. Do not override release
    codegen-units; the repository's release/LTO profile is part of the
    measurement.

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

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

Dimension Required cells
Key o.x, o["x"], stable o[k], alternating dynamic keys
RHS 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.
  • The canonical perf: beat Node on object-write micro (#6809) #6811 micro remains correct and shows no material
    regression (target: within 10% on the same machine/protocol).
  • 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.

Validation gates

At minimum for every implementation PR:

cargo fmt --all -- --check
git diff --check
python3 scripts/gc_store_site_inventory.py --self-test
python3 scripts/gc_store_site_inventory.py
cargo test -p perry-codegen --test native_proof_regressions
cargo test -p perry-codegen --test shadow_slot_hygiene
cargo test -p perry-runtime --lib -- --test-threads=1
./scripts/run_gap_tests.sh

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.rs
    • static_put_value_uses_write_pic_for_call_free_rhs
    • static_put_value_rejects_write_pic_when_rhs_can_allocate
    • nested_same_shape_object_writes_version_the_whole_loop
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
    • immutable_index_alias_binds_once_but_keeps_incremental_root_barrier
  • crates/perry-runtime/src/proxy.rs
    • object_array_numeric_write2_guard_requires_complete_uniform_proof
  • crates/perry-runtime/src/gc/tests/barrier.rs
    • incremental-mark active-count coverage

Session-start checklist

  1. Read this issue, Architecture: adopt V8's object-model construction — explicit runtime state, self-describing headers, shape tree (phases A–C) #6759, Object-op hot paths: TLS fetch density (~50% of write samples) + hot-path mutex — Phase A acceptance completion (#6759) #6809, and merged PR perf: beat Node on object-write micro (#6809) #6811.
  2. Create a fresh isolated worktree/branch from current origin/main; confirm
    it contains 20f19758f.
  3. Use an isolated Cargo target directory. Check that no old build/benchmark
    process is still running.
  4. Reproduce the canonical 5 ms Perry / 7 ms Node result before changing code.
    If it does not reproduce, diagnose the baseline first.
  5. Build and post the coverage/rejection matrix.
  6. Choose one measured slice, keep its PR narrow, and include a changelog
    fragment plus benchmark/test evidence.

Related work

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.

Activity

  1. added
    performanceRuntime, compile-time, build-size, or memory performance
    on Jul 23, 2026
  2. proggeramlug commented on Jul 23, 2026

    @proggeramlug
    ContributorAuthor

    #6812 pre-implementation coverage baseline

    Collected on 2026-07-23 before changing any compiler or runtime source.

    Provenance and protocol

    • Source: origin/main commit
      20f19758f370f7616530824218d87f46a67cd3f0, which is the required perf: beat Node on object-write micro (#6809) #6811
      merge.
    • Clean worktree branch: perf/6812-object-write-generalization.
    • Isolated Cargo target:
      /private/tmp/perry-6812-target.qyHg3D.
    • Compiler: Perry 0.5.1264, built with the issue's exact release command and
      the repository release/LTO profile.
    • App compilation used the default auto-optimizing pipeline. Neither
      PERRY_NO_AUTO_OPTIMIZE nor a codegen-unit override was set.
    • Host: macOS 26.5 (25F71), Apple M1 Max, arm64, 10 logical CPUs.
    • Node: v26.3.0.
    • Rust: rustc 1.96.1, cargo 1.96.1.
    • Each matrix cell had one Node/Perry warmup followed by 15 strict
      Node-then-Perry pairs. The runner compared (writes, checksum) after every
      pair and retained all samples without outlier filtering.
    • No old Perry, Node, Cargo, rustc, or matrix process was active when the
      definitive capture began. Sporadic OS/UI interruptions remain visible in
      the raw data instead of being discarded.

    The exact historical canonical source produced:

    Implementation Raw write_ms samples Median Sink
    Node [7, 8, 7, 7, 8, 7, 8, 8, 7, 7, 8, 8, 8, 8, 7] 8 ms 9595200
    Perry [5, 6, 6, 5, 6, 6, 6, 5, 6, 6, 6, 6, 5, 6, 6] 6 ms 9595200

    The one-millisecond shift from the published 7/5 medians is Date.now()
    quantization at this duration; Perry still beats Node on the exact unchanged
    source.

    Executable matrix

    Dimension Cell Writes Node median Perry median
    Key o.x 120000000 131 ms 689 ms
    Key o["x"] 120000000 132 ms 693 ms
    Key stable o[k] 120000000 138 ms 2425 ms
    Key alternating dynamic keys 24000000 183 ms 625 ms
    RHS numeric scalar 120000000 133 ms 698 ms
    RHS pointer-capable value 96000000 120 ms 1480 ms
    RHS allocating literal 19200000 119 ms 11126 ms
    RHS function call 108000000 121 ms 3260 ms
    Receiver shapes monomorphic 120000000 135 ms 706 ms
    Receiver shapes 2-shape 96000000 131 ms 5432 ms
    Receiver shapes 4-shape 60000000 108 ms 3330 ms
    Receiver shapes transition before loop 120000000 131 ms 681 ms
    Fields/iteration 1 120000000 131 ms 692 ms
    Fields/iteration 2 240000000 148 ms 138 ms
    Fields/iteration 4 384000000 137 ms 1444 ms
    Fields/iteration 8 384000000 102 ms 1272 ms
    Loop form single counted loop 120000000 163 ms 2249 ms
    Loop form current nested loop 240000000 149 ms 138 ms
    Loop form stable local bounds 192000000 129 ms 868 ms
    Loop form non-zero inner start 192000000 120 ms 872 ms
    Storage inline existing slot 120000000 132 ms 693 ms
    Storage wide/overflow object 120000000 180 ms 7154 ms
    Receiver kind anonymous object 120000000 109 ms 692 ms
    Receiver kind class instance 120000000 108 ms 700 ms
    Receiver kind class-id-zero plain object 120000000 110 ms 6356 ms

    Every one of the 375 measured Node/Perry pairs had identical write counts and
    checksums.

    Deterministic path classification

    The retained 856,485-byte pre-optimization LLVM module was classified by
    function-local calls to js_put_value_set, js_put_value_set_ic_miss, and
    js_object_array_numeric_write2_guard. The timed loop was also inspected
    separately when its function contains setup writes.

    Cell Timed path IR calls (generic, PIC, clone) Reason / rejection
    o.x static PIC 0, 1, 0 Literal key and call-free numeric RHS.
    o["x"] static PIC 0, 1, 0 Lowers to the same literal-key form as dot access.
    stable o[k] generic 1, 0, 0 Key is a local expression, not Expr::String.
    alternating keys generic 1, 0, 0 Dynamic PropertyKey conversion/identity stays semantic.
    numeric RHS static PIC 0, 1, 0 Safepoint-free scalar value.
    pointer RHS static PIC 0, 1, 0 Safepoint-free, but retains layout/string-alias/barrier handling.
    allocating RHS generic 1, 0, 0 RHS can allocate after the receiver reference is evaluated.
    function RHS generic 1, 0, 0 RHS call is a safepoint and may throw/observe user code.
    monomorphic shape static PIC 0, 1, 0 Stable discriminated ShapeId token primes and hits.
    2-shape static PIC, runtime miss 0, 1, 0 One-entry cache thrashes on alternating shape tokens.
    4-shape static PIC, runtime miss 0, 1, 0 One-entry cache thrashes on four shape tokens.
    transition before loop static PIC 0, 2, 0 Setup transition has its own site; timed site sees one stable post-transition token.
    1 field static PIC 0, 1, 0 Whole-loop matcher requires exactly two stores.
    2 fields whole-loop clone 0, 2, 1 Once-per-nest proof succeeds; both PIC sites remain in the semantic clone.
    4 fields four static PICs 0, 4, 0 Whole-loop matcher requires exactly two stores.
    8 fields eight static PICs 0, 8, 0 Whole-loop matcher requires exactly two stores.
    single counted loop static PIC 0, 1, 0 Whole-loop proof requires the dense nested-array form.
    current nested loop whole-loop clone 0, 2, 1 Exact #6811 shape.
    stable local bounds static PICs 0, 2, 0 Counted-loop matcher requires literal bounds.
    nonzero inner start static PICs 0, 2, 0 Dense-prefix proof deliberately starts at zero.
    inline storage static PIC 0, 1, 0 Existing slot is within the physical inline allocation.
    overflow storage static PIC, runtime miss 1 setup, 1, 0 Dynamic setup is generic; timed slot 4 cannot prime against a four-slot inline allocation.
    anonymous object static PIC 0, 1, 0 Compiler-created regular object has a nonzero class/shape identity.
    class instance static PIC 0, 1, 0 Eligible regular instance with stable class/shape identity.
    class-id-zero object static PIC, runtime miss 0, 1, 0 Parsed plain object fails the runtime class_id != 0 eligibility guard.

    The key measured discontinuity is the bounded static numeric nest:

    • two fields: 240M stores in 138 ms through the transactional clone;
    • one field: 120M stores in 692 ms through a per-store PIC;
    • four fields: 384M stores in 1444 ms through four per-store PICs.

    The four-field cell is the highest-volume sound miss in the matrix and is
    10.5× slower than Node, while the established two-field proof is already
    slightly faster than Node.

    Sample evidence

    The stripped executable was linked once more with PERRY_LINK_MAP (which does
    not alter generated code) and sampled with macOS sample.

    For stable dynamic-key writes, 1,204 of 1,466 main-thread samples (82.1%) were
    under js_put_value_set. Of those, 519 reached
    try_existing_own_data_overwrite, and 108 were under
    runtime_store_jsvalue_slot, with further frames in layout-note and write
    barrier work. The addresses were resolved against these linker-map ranges:

    0x100489E00 + 0xBBC  js_put_value_set
    0x100298750 + 0x2D0  try_existing_own_data_overwrite
    0x10014AA44 + 0x0AC  runtime_store_jsvalue_slot
    

    For the four-field static cell, 441 of 448 post-startup runnable samples
    (98.4%) were directly in the inlined main body, with no child runtime frame.
    Together with the four PIC call sites retained in IR, this shows the cost is
    the repeated inlined per-store guard sequence, not a generic-runtime call.
    The two-field executable result demonstrates the benefit of paying a bounded
    preflight once and then keeping the complete nest call/GC-free.

    Baseline code size

    Artifact Size
    Matrix LLVM IR 856,485 bytes / 21,966 lines
    Matrix app object 278,968 bytes
    Matrix executable 6,302,720 bytes
    Linked main text contribution 47,232 bytes
    Canonical executable 6,170,504 bytes
    All raw matrix samples and checksums
    Cell Checksum Node ms Perry ms
    key_dot 122876400 [132, 131, 130, 132, 132, 131, 130, 131, 133, 132, 131, 131, 130, 132, 131] [690, 688, 690, 691, 688, 689, 688, 711, 701, 688, 690, 688, 689, 689, 695]
    key_literal 122876400 [131, 134, 132, 133, 132, 134, 132, 131, 131, 132, 132, 132, 133, 131, 131] [692, 691, 693, 693, 701, 690, 689, 695, 691, 692, 693, 694, 693, 701, 691]
    key_stable_dynamic 122876400 [138, 138, 140, 139, 136, 136, 135, 148, 140, 141, 138, 136, 139, 137, 140] [2425, 2448, 2431, 2384, 2395, 2383, 2653, 2549, 2442, 2456, 2423, 2419, 2399, 2408, 2435]
    key_alternating_dynamic 31194000 [183, 266, 184, 179, 194, 185, 183, 312, 182, 181, 178, 183, 181, 182, 180] [637, 796, 840, 846, 623, 625, 790, 667, 610, 613, 605, 644, 614, 607, 610]
    rhs_numeric 122876400 [133, 139, 132, 134, 136, 133, 132, 132, 133, 133, 132, 132, 133, 134, 133] [699, 711, 697, 702, 698, 697, 700, 696, 706, 697, 684, 683, 695, 701, 712]
    rhs_pointer 40800 [121, 120, 119, 120, 120, 120, 119, 120, 119, 121, 121, 122, 122, 119, 120] [1487, 1488, 1480, 1480, 1477, 1481, 1478, 1482, 1480, 1478, 1515, 1482, 1481, 1479, 1479]
    rhs_allocating 22076400 [119, 114, 153, 143, 134, 120, 117, 113, 117, 113, 112, 113, 142, 195, 119] [10936, 12509, 16052, 13348, 11307, 11055, 10870, 10729, 10714, 10823, 10733, 11126, 15021, 13009, 11158]
    rhs_call 110876400 [123, 191, 124, 119, 126, 119, 118, 121, 129, 120, 129, 119, 145, 120, 121] [3309, 3418, 3311, 3214, 3217, 3260, 3256, 3264, 3300, 3243, 3250, 3589, 3864, 3235, 3218]
    shape_monomorphic 122876400 [135, 135, 135, 133, 136, 134, 135, 137, 133, 136, 133, 135, 135, 135, 135] [709, 708, 705, 706, 721, 707, 713, 700, 697, 691, 705, 709, 695, 712, 689]
    shape_two 98876400 [130, 131, 150, 131, 130, 134, 137, 131, 130, 132, 134, 132, 130, 132, 130] [5330, 5963, 5401, 5432, 7717, 5527, 5456, 5429, 5440, 5433, 5432, 5368, 5320, 5352, 5326]
    shape_four 62876400 [107, 106, 108, 109, 107, 108, 109, 106, 109, 110, 114, 109, 106, 108, 107] [3330, 3332, 3323, 3327, 3395, 3337, 3327, 3341, 3392, 3373, 3387, 3317, 3284, 3296, 3288]
    shape_transition_before_loop 122876400 [130, 131, 131, 131, 131, 131, 133, 131, 130, 133, 131, 132, 132, 132, 132] [679, 680, 691, 680, 682, 680, 681, 682, 680, 681, 682, 682, 681, 681, 682]
    fields_one 122876400 [131, 132, 130, 131, 132, 131, 131, 131, 131, 132, 132, 131, 130, 131, 131] [691, 695, 693, 692, 690, 689, 692, 689, 689, 693, 713, 691, 692, 693, 690]
    fields_two 239995200 [149, 148, 150, 148, 147, 148, 149, 150, 148, 148, 148, 148, 147, 149, 149] [138, 138, 139, 139, 137, 138, 137, 138, 137, 138, 138, 138, 138, 137, 140]
    fields_four 383990400 [136, 136, 137, 136, 137, 137, 136, 137, 137, 137, 137, 137, 137, 141, 138] [1432, 1446, 1447, 1444, 1441, 1443, 1446, 1445, 1444, 1446, 1443, 1442, 1446, 1455, 1441]
    fields_eight 383980800 [102, 102, 101, 100, 101, 102, 104, 102, 102, 112, 101, 103, 102, 114, 102] [1260, 1262, 1259, 1260, 1273, 1272, 1270, 1661, 1782, 1276, 1591, 1276, 1272, 1266, 1264]
    loop_single_counted 287997118800 [164, 163, 164, 163, 163, 163, 163, 164, 164, 164, 164, 162, 163, 162, 163] [2286, 2258, 2247, 2250, 2251, 2254, 2248, 2246, 2248, 2247, 2247, 2249, 2249, 2248, 2253]
    loop_current_nested 239995200 [150, 149, 148, 150, 149, 150, 148, 151, 148, 148, 148, 151, 149, 150, 149] [138, 139, 138, 138, 138, 139, 138, 138, 138, 137, 140, 138, 137, 138, 138]
    loop_stable_local_bounds 191995200 [130, 129, 129, 129, 129, 128, 129, 130, 130, 130, 132, 130, 130, 129, 128] [882, 865, 868, 873, 869, 916, 864, 868, 867, 868, 869, 882, 887, 866, 864]
    loop_nonzero_start 191995200 [119, 122, 119, 120, 119, 121, 120, 120, 119, 120, 121, 120, 120, 121, 119] [872, 876, 872, 874, 887, 871, 872, 871, 871, 871, 872, 887, 873, 872, 886]
    storage_inline 122876400 [132, 131, 131, 132, 132, 133, 132, 142, 136, 133, 132, 134, 132, 131, 132] [692, 693, 693, 694, 692, 693, 695, 694, 693, 686, 684, 682, 695, 692, 692]
    storage_overflow 122876400 [180, 180, 178, 179, 178, 180, 190, 179, 180, 180, 179, 179, 181, 191, 179] [7169, 7120, 7171, 7149, 7167, 7142, 7154, 7121, 7130, 7139, 7165, 7135, 7174, 7432, 7163]
    receiver_anonymous 122876400 [110, 110, 108, 109, 109, 110, 108, 109, 109, 108, 110, 108, 109, 109, 110] [692, 693, 693, 692, 692, 692, 693, 693, 692, 693, 691, 694, 692, 692, 698]
    receiver_class_instance 122876400 [105, 108, 109, 107, 107, 108, 109, 144, 106, 114, 110, 108, 107, 113, 108] [697, 695, 707, 705, 695, 687, 700, 950, 897, 896, 700, 700, 956, 691, 706]
    receiver_class_id_zero 122876400 [110, 111, 106, 105, 106, 107, 120, 111, 107, 107, 1125, 134, 119, 106, 137] [6381, 6672, 6203, 6112, 6106, 6596, 6356, 6217, 6198, 42809, 20211, 7303, 6197, 6235, 7880]
  3. proggeramlug commented on Jul 24, 2026

    @proggeramlug
    ContributorAuthor

    #6812 bounded object-write generalization: final evidence

    This implements measured slice B from the pre-implementation matrix: generalize the #6811 whole-nest proof from exactly two stores to a bounded one-through-four static numeric field descriptor.

    Implementation and safety boundary

    • The compiler preserves source order and duplicate keys, proves every arithmetic intermediate finite across the complete literal loop domain, performs one preflight, and emits a finite call/GC/side-exit-free clone.
    • The runtime validates the dense receiver prefix, shared keys array, regular nonzero-class objects, descriptor/frozen/sealed/no-extend state, physical INLINE_SLOT_FLOOR = 4 bounds, and intact typed descriptors before returning four packed 16-bit slot lanes.
    • Finite numeric bits are valid in both verified raw-f64 fields and ordinary JSValue fields. A forged/stale typed-layout-intact bit without the side-table descriptor rejects.
    • The once-only key lookup accepts a live non-forwarded string-pool key and compares content without allocation or retention. Requiring INTERNED made otherwise identical x/y fields miss depending on unrelated interning.
    • The prior js_object_array_numeric_write2_guard export and its 32-bit lane ABI remain available for cached generated objects; new code uses the generalized guard.
    • PERRY_TRACE_OBJECT_ARRAY_WRITE_GUARD=1 prints a deterministic cold rejection reason. Successful guards do no environment lookup or logging.
    • A failed proof enters the untouched semantic clone before the first store. Dynamic keys, safepointing/pointer RHS values, mixed shapes, holes, short arrays, accessors/setters, proxies, readonly/frozen/sealed/non-extensible objects, overflow fields, class-id-zero objects, nonliteral/nonzero bounds, nonfinite arithmetic, and five-or-more-store bodies retain fallback semantics.

    The generated fast stores retain GC_STORE_AUDIT(POINTER_FREE), and function-local IR inspection found zero calls in all 16 generated fast inner blocks.

    15-pair executable measurements

    Same clean worktree/host/isolated release target/default auto-optimization as the baseline. One warmup, then 15 strict Node-then-Perry pairs per cell. Every pair matched write count and checksum.

    Fields/iteration Baseline Perry Final Node Final Perry Change
    1 692 ms 133 ms 122 ms 5.67x faster
    2 138 ms 150 ms 139 ms +0.7% (noise)
    4 1,444 ms 142 ms 127 ms 11.37x faster
    8 (bounded rejection) 1,272 ms 102 ms 1,271 ms -0.1% (unchanged)

    Raw alternating samples:

    Cell Checksum Node ms Perry ms
    fields_one 122876400 [130, 133, 133, 130, 131, 131, 136, 137, 135, 135, 133, 138, 135, 133, 138] [121, 121, 120, 121, 124, 124, 123, 121, 122, 121, 123, 126, 123, 122, 119]
    fields_two 239995200 [170, 172, 148, 150, 148, 149, 149, 152, 149, 151, 147, 148, 158, 150, 154] [155, 149, 139, 137, 138, 139, 139, 138, 137, 137, 140, 144, 139, 138, 144]
    fields_four 383990400 [140, 143, 145, 141, 145, 142, 138, 142, 140, 136, 142, 158, 151, 136, 136] [131, 132, 125, 126, 130, 124, 127, 126, 127, 130, 131, 140, 137, 123, 123]
    fields_eight 383980800 [100, 100, 100, 106, 104, 101, 103, 100, 102, 101, 104, 107, 107, 102, 102] [1260, 1265, 1268, 1273, 1271, 1272, 1274, 1270, 1269, 1278, 1267, 1274, 1271, 1273, 1279]

    The exact unchanged canonical source also retained its baseline median:

    Implementation Raw write_ms samples Median Sink
    Node [8, 7, 8, 8, 8, 8, 9, 9, 7, 8, 7, 7, 7, 8, 8] 8 ms 9595200
    Perry [6, 5, 6, 6, 6, 6, 5, 7, 6, 6, 5, 6, 5, 6, 6] 6 ms 9595200

    The new one- and four-field cells beat Node under the required protocol. The established two-field/canonical paths show no material regression, while the eight-field forced fallback remains unchanged.

    IR and code size

    Definitive uncached matrix IR:

    • fieldsOne: one generalized guard / one fallback PIC
    • fieldsTwo: one generalized guard / two fallback PICs
    • fieldsFour: one generalized guard / four fallback PICs
    • fieldsEight: no generalized guard / eight fallback PICs
    • 16 fast inner blocks, zero calls in those blocks
    • zero calls to the old two-field guard from new code
    Artifact Baseline Final Growth
    Matrix LLVM IR 856,485 B / 21,966 lines 892,819 B / 22,764 lines +4.24% / +3.63%
    Matrix app object 278,968 B 284,800 B +2.09%
    Linked main text 47,232 B 49,012 B +3.77%
    Matrix executable 6,302,720 B 6,302,720 B 0%
    Canonical executable 6,170,504 B 6,170,512 B +8 B

    A final one-pair sweep of all 25 matrix cells had 25/25 Node/Perry checksum matches. Dynamic/safepointing, polymorphic, wide-storage, variable-bound, eight-field, and class-id-zero cells stayed on their intended prior paths.

    Validation

    • cargo fmt --all -- --check
    • git diff --check
    • python3 scripts/gc_store_site_inventory.py --self-test
    • full GC store inventory: 1,161 files, 245 audited sites, 65 allowlisted
    • cargo test -p perry-codegen --test native_proof_regressions: 248 passed
    • cargo test -p perry-codegen --test shadow_slot_hygiene: 10 passed
    • cargo test -p perry-runtime --lib -- --test-threads=1: 1,460 passed, 3 ignored
    • library-target Clippy completed for both changed crates. An exploratory --all-targets -D warnings run is currently blocked by existing lints in untouched build/dependency/test files.
    • final 20-line adversarial corpus: exact Node parity in default mode and with PERRY_DISABLE_CLASS_FIELD_INLINE=1, PERRY_TYPED_FEEDBACK=1, and PERRY_VERIFY_TYPED_INTACT=1
    • full gap driver: 387 parity passes, 0 compile failures, 0 crashes; all seven reported mismatches were recognized as already known/triaged, and the driver ended with Gap gate OK
    • final expanded perf: generalize guarded object-write fast paths beyond the #6811 two-field loop #6812 gap test (including inherited setters and Proxy traps): focused release-driver rerun passed

    The worktree contains the reusable benchmark runner, immutable baseline, final raw evidence, focused Rust/IR regressions, adversarial gap corpus, and changelog fragment.

  4. proggeramlug commented on Jul 24, 2026

    @proggeramlug
    ContributorAuthor

    Coverage/rejection matrix (issue step 5) — measured triage pass on main @ 8bda0351e, node v26.3.0/arm64, medians of 3 alternated runs, identical checksums on all 18 cells. Full table + code-gate provenance: docs/object-write-matrix.md (committed on perf/6812-write-matrix); raw samples preserved. Highlights:

    Beating node already (5/18): the whole-loop-clone family (canonical 0.67×, one-write 0.71×, three-writes 0.78×, stable-dynkey 0.75×) and class instances via the PIC (0.50× — best row).

    Top gaps, ranked by breadth:

    1. Builder pattern ({} + N assigns): 186× — class_id==0 blocks the PIC and every assign is a generic transition. The single most ubiquitous real-world write shape.
    2. Overflow-slot writes (wide objects): 180× — PIC bounds guard rejects; generic per write.
    3. Integer keys on objects: 12×.
    4. Clone-eligibility family (mul-RHS 6.7×, dyn bound 6.4×, while 6.6×, call-in-body 14×, mutable alias 8.8×): one narrow matcher rule each; shared floor is the per-write PIC at ~10 ns.
    5. Helper-fn monomorphic 5.9× / poly-2 4.5×: call overhead + guard chain vs node's inlining.

    Near-node, deprioritized: megamorphic 8-shape (1.4×), allocating RHS (1.4× — much better than the discarded prototype implied).

    Proceeding per issue step 6 with the top slice: the builder pattern (fresh-object transition writes). 15-run idle-protocol evidence will accompany each PR.

  5. proggeramlug commented on Jul 25, 2026

    @proggeramlug
    ContributorAuthor

    First matrix slice is up: #6829 — builder-sequence literal fold. Worst row (builders, 186×) → 25.4× vs node (15-run medians on the PR); zero matrix regressions, gap gate OK, all suites green. Next slice per the ranking: the clone-eligibility family (mul-RHS / dynamic bound / while / mutable alias — 4 rows, one matcher generalization) targeting the 0.7× class their siblings occupy.

  6. proggeramlug commented on Jul 25, 2026

    @proggeramlug
    ContributorAuthor

    Slice 2 up: #6830 — clone-eligibility widening (Mul RHS / let alias / while form). Three rows move to 0.62–0.75× (beating node); 18-cell sweep regression-free, gap gate OK. 8 matrix rows now beat node. Next: dynamic inner bounds (w4, 6.25×) — needs the loop-invariant runtime bound extension to the versioned emitter; then the overflow-slot row (153×).

  7. added 7 commits that reference this issue on Jul 25, 2026
  8. added
    triagedMaintainer reviewed; type, scope, and next step are clear
    on Jul 26, 2026
  9. added a commit that references this issue on Jul 26, 2026
  10. 13 remaining items

  11. proggeramlug commented on Aug 14, 2026

    @proggeramlug
    ContributorAuthor

    Status audit against origin/main @ 514ff7c9a — what landed, what is left, and what in this issue is now false

    1. The required first step was completed, and the matrix is a maintained artifact

    docs/object-write-matrix.md was created on main by #6829 and has been updated
    by every slice since; last touched 1bfd41171 (#8026, 2026-08-13).

    2. Twelve commits of coverage work landed under this issue

    1bfd41171 2026-08-13 perf: settle eight-shape static write PICs (#8026)
    c9d961fbd 2026-07-27 #6812 key-table clone lane: w12 beats node 6x (#6902)
    b5727f305 2026-07-27 #6812 3-way dynamic-key write IC (#6895)
    b9bfb9ac5 2026-07-27 #6812 multi-group clone (w9) + inliner PutValueSet arm (w6) (#6868)
    563c35951 2026-07-26 #6812 spill lanes -> append-past-capacity clone-eligible (w13) (#6856)
    6285f552f 2026-07-26 #6812 object-owned overflow storage replaces the TLS side table (#6849)
    6ff01001f 2026-07-26 #6812 w8/w13 inlined-helper clone, integer static keys, peel (#6841)
    60e4e2c20 2026-07-25 #6812 w16 per-site builder identity, learned width (#6833)
    9b15dc1e2 2026-07-25 #6812 fold builder sequences into object literals (#6829)
    f6fb9a48a 2026-07-24 bounded polymorphic object-write PIC (#6823)
    5c86a26c6 2026-07-24 specialize immutable numeric loop bounds (#6822)
    20f19758f 2026-07-23 beat Node on object-write micro (#6809) (#6811)
    

    Both items the last comment named as "next" landed. Dynamic inner bounds
    (w4)
    is easy to miss: PR #6830's merge commit 08dab1e0d is not an ancestor
    of main — #6829's squash swallowed it (git log -S 'dyn_len_source' →
    9b15dc1e2). Overflow-slot (w16) landed across #6833 / #6849 / #6856,
    173x → 0.26.

    Twelve un-folded changelog.d/6812-* fragments are on main.

    3. What the two implementation sites accept today

    Both functions named in the issue body were renamed
    (…write2… → …write…); the old js_object_array_numeric_write2_guard survives
    only as an ABI shim (proxy/put_value.rs:1146).

    Whole-loop clone — matcher stmt/loops.rs:2369, emitter :2879.
    Bounds: MAX_OBJECT_ARRAY_WRITE_FIELDS = 4 (:2043),
    MAX_OBJECT_ARRAY_WRITE_GROUPS = 2 (:2118),
    MAX_OBJECT_ARRAY_WRITE_TEMPS = 8, MAX_…_NUMBER_NODES = 64.
    Now accepted beyond the issue's list: dynamic xs.length inner bound
    (:2415-2444), while inner form (:2460-2491), let aliases (:2518),
    two groups, Mul/Mod in the RHS grammar, const k = "c" and constant-integer
    keys (:2722, :2729), the w12 key-table lane (:2643-2712), and a
    first-iteration peel (:2893-2937).
    Still rejected: non-zero inner start (:2495), the single non-nested counted
    loop (:2382-2491), a dynamic outer bound, >4 fields, >2 groups, division,
    non-finite intermediates.

    Static write PIC — expr/proxy_reflect.rs:426, miss proxy/put_value.rs:277.
    Now 4 inline + 4 outlined ways (STATIC_PIC_TAIL_WAYS = 4) with a
    non-evicting tail; 9th shape falls to full [[Set]]. New sibling: a 3-way
    dynamic-key IC
    (js_put_value_set_dyn_ic, :474).
    Still requires class_id != 0 and idx < max(field_count, INLINE_SLOT_FLOOR) —
    overflow/spill slots remain PIC-ineligible (:392, :555); only the loop
    clone learned spill lanes.

    4. ⚠ A load-bearing invariant in this issue's body is now FALSE

    INLINE_SLOT_FLOOR is 4, not 8. Never over-read small object allocations.

    It is 2 since f110261a4 (#7928, 2026-08-12, "right-size small objects —
    INLINE_SLOT_FLOOR 4 → 2"):
    perry-codegen/src/target_layout.rs:71, perry-runtime/src/object/mod.rs:49.
    The PIC's alloc_limit (put_value.rs:390) and the loop guard's
    first_limit/limit (:927, :984) both derive from it, so a 3rd/4th slot on
    a small object now falls outside inline storage and depends on the spill path.
    No matrix cell was re-measured after #7928. A stale comment also survives at
    perry-runtime/src/object/tests.rs:625.

    5. What is genuinely still open — and nothing else owns it

    I checked all 69 open issues and all 12 open PRs: no other issue or PR covers
    overflow-slot writes, dynamic loop bounds, integer keys, the builder pattern,
    w15's residual, w10's post-#8026 gap, slice A, non-zero inner start, or the
    single-counted-loop form. This issue is the sole owner of all of it, and every
    acceptance checkbox is still unchecked.

    1. w15_append_build — the builder residual. perf(hir): #6812 — fold straight-line builder sequences into object literals #6829 took it 186x → 25x and
      the row says "residual tracked below", but nothing tracks it. There is an
      abandoned branch on origin, perf/6812-w15-anon-init @ 98fb60d17
      (2026-07-26, PR never opened), whose commit message records the profile:
      "w15 profile: guard+contract 27, hashmap 17, layout notes 20 of 176
      samples"
      — i.e. the residual is allocation-side (class-field guard
      revalidation + per-slot layout notes in the synthesized anon-shape
      constructor), not write-side. That is perf(GC): make per-object layout O(1)-loadable — kill per-operation thread-local layout tracking (umbrella: method_calls/array-downgrade/object-property) #5094/perf(gc): layout side tables are 34% of object construction — the construction/death half of #5094 (allocation is 7.7%) #7510 territory, and most of it
      has been re-worked since (perf(gc): skip the per-object layout side tables while they are provably empty (#7510) #7525/perf(codegen,gc): declare a class's typed-shape layout at allocation, not after its constructor (#7510 item 1, #7512) #7532/perf(gc): make a repeat typed-shape construction a header bit-set (#7510) #7535/perf(runtime): reach the hot-TLS cache without _tlv_get_addr (#7469 structural half) #7565/perf(gc): stop re-deriving compile-time mask facts on every construction (#7578) #7586/perf(codegen): stamp a pointer-free shape's typed layout into the allocation header #7834), so the 25x
      number is very likely stale.
    2. w10_poly8 ≈ 3.4x. From perf: settle eight-shape static write PICs #8026's own PR body, on its new 60M-write
      shape_eight cell: Node 226 ms vs Perry 768 ms (down from 2,359 ms).
      The matrix's "1.4" is a pre-tail small-cell number that understates it.
    3. Slice A (root the PutValue reference across a safepointing RHS) never
      landed.
      expr/proxy_reflect.rs:441-448 still carries the original
      rejection verbatim — "Until PutValue reference temporaries have dedicated
      GC roots…"
      — and proxy/put_value.rs:465 records the safepointing-RHS
      approach as tried and abandoned. Worth revisiting now that the Layer-1
      rooting-by-construction API exists (Layer 1 campaign: migrate perry-codegen's lowerings onto the rooting-by-construction API (88 modules, 262 hazard sites) #7615). This is w17 (1.4x) plus the
      baseline matrix's rhs_call and rhs_allocating cells.
    4. Two loop forms from the original baseline have no w-row and are still
      rejected
      : non-zero inner start (loops.rs:2495; 7.3x at baseline) and the
      single non-nested counted loop (loops.rs:2382; 13.8x at baseline).

    6. The matrix numbers are stale as evidence

    Measured on 8bda0351e (2026-07-24) against Node 26.3.0 — the repo now pins
    26.5.1 — with 3-run medians at ambient load ~7. The raw CSV the header cites
    (matrix_raw_2026-07-25.csv) is not committed, and the w-cell harness was
    never committed
    : benchmarks/object-write-6812/matrix.ts is the older 26-cell
    dimension matrix and has no w15/w16 cells. The doc's prose is also internally
    stale — "Two catastrophic rows (~180x) … the builder pattern and wide-object
    overflow-slot writes" sits directly above a table where w16 reads 0.26.


    7. Fresh 26-cell matrix on current main (v0.5.1510, Node 26.5.1)

    Using the committed harness, benchmarks/object-write-6812/{matrix.ts,run_matrix.py},
    --samples 3 --warmups 1, against a compiler and both static archives built
    from a clean worktree at 514ff7c9a with PERRY_RUNTIME_DIR pinned and the
    default auto-optimizing pipeline. The runner verifies (writes, sink) on every
    Node/Perry pair and aborts on mismatch; it ran to completion, so all 26 cells
    are output-identical
    . The host is contended, so read ratios, not absolutes.

    cell                                 node   perry  P/node
    receiver_class_id_zero                163   12709   77.97
    rhs_call                              513    9376   18.28
    loop_single_counted                   375    4954   13.21
    fields_eight                          352    4164   11.83
    rhs_pointer                           635    5848    9.21
    loop_nonzero_start                    338    2369    7.01
    rhs_allocating                        548    2540    4.64
    shape_two                             398    1808    4.54
    shape_four                            346    1332    3.85
    key_alternating_dynamic               939    3274    3.49
    shape_eight                           996    3463    3.48
    storage_overflow                      505     699    1.38
    fields_two                            717     590    0.82  BEATS
    key_literal                           988     781    0.79  BEATS
    rhs_numeric                           585     459    0.78  BEATS
    key_stable_dynamic                    976     735    0.75  BEATS
    key_dot                              1054     742    0.70  BEATS
    loop_current_nested                   412     277    0.67  BEATS
    fields_one                           1066     673    0.63  BEATS
    storage_inline                        357     220    0.62  BEATS
    fields_four                           892     524    0.59  BEATS
    shape_transition_before_loop          681     363    0.53  BEATS
    loop_stable_local_bounds              381     192    0.50  BEATS
    receiver_class_instance               301     150    0.50  BEATS
    receiver_anonymous                    462     228    0.49  BEATS
    shape_monomorphic                     446     161    0.36  BEATS
    
    beats node: 14 of 26
    

    Against the pre-implementation baseline posted on this issue:

    cell 2026-07-23 now
    rhs_allocating 93.5x 4.64x
    shape_two 41.5x 4.54x
    storage_overflow 39.7x 1.38x
    shape_four 30.8x 3.85x
    rhs_call 26.9x 18.3x
    loop_single_counted 13.8x 13.2x flat
    fields_eight 12.5x 11.8x flat
    rhs_pointer 12.3x 9.2x
    loop_nonzero_start 7.3x 7.0x flat
    key_alternating_dynamic 3.4x 3.5x flat
    receiver_class_id_zero 57.8x 78.0x ★ not improved

    8. ★ The biggest remaining gap is JSON.parse receivers, and it is a 55x

    intra-matrix difference at an identical write count and checksum

    receiverClassIdZero (matrix.ts:501-515) differs from keyDot in exactly one
    way: its 2400 receivers come from JSON.parse('{"x":0}') rather than an object
    literal. Same 120,000,000 writes, same sink:

    $ PERRY_TRACE_OBJECT_ARRAY_WRITE_GUARD=1 ./bin_matrix receiver_class_id_zero
    PERRY_OBJECT_ARRAY_WRITE_GUARD_REJECT: first receiver is not an eligible regular shared-shape object
    cell receiver_class_id_zero ms 9560 writes 120000000 sink 122876400
    
    $ PERRY_TRACE_OBJECT_ARRAY_WRITE_GUARD=1 ./bin_matrix key_dot
    cell key_dot ms 173 writes 120000000 sink 122876400
    

    9560 ms vs 173 ms. A JSON.parse-produced object has class_id == 0, which
    disqualifies it from the whole-loop clone (the guard's per-element validation)
    and from the static write PIC (proxy/put_value.rs:337-341), so every one of
    the 120M writes takes the generic runtime path. Every API response, config blob
    and ORM row in real code enters a program this way. This is squarely #8067's
    subject ("make ShapeId authoritative for keys, live slots, and class-object
    kind") and should be sequenced behind it rather than raced.

    Worth separating from the doc's "builder pattern" framing, which conflates two
    different things: the HIR builder fold (#6829) works — a straight-line
    {} + 6 static assigns over 2M escaping objects measures at node parity
    (node 820/791/874 ms vs Perry 859/827/687 ms, sink 38000100 identical) — while
    class_id == 0 receivers still cost 55x, and JSON.parse rather than
    {} + assigns is how you get one today.

    9. Recommendation

    The closing condition in the body 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."
    The first two
    are met — the matrix is a maintained in-repo artifact and twelve commits landed
    against it, taking four cells from 27–94x to 1.4–4.6x. The third is not: the
    twelve non-beating cells above are unfinished, not justified.

    Given the repo's preference for granular tickets over umbrellas, I'd suggest
    closing this one and carrying the residue as focused issues with the numbers
    above:

    1. class_id == 0 receivers (JSON.parse, 78x) — sequence behind feat(object/shape): make ShapeId authoritative for keys, live slots, and class-object kind before #8047 #8067.
    2. Slice A: root the PutValue reference across a safepointing RHS — rhs_call
      18.3x, rhs_pointer 9.2x, rhs_allocating 4.6x. The rejection comment at
      expr/proxy_reflect.rs:441-448 predates the Layer-1 rooting-by-construction
      API (Layer 1 campaign: migrate perry-codegen's lowerings onto the rooting-by-construction API (88 modules, 262 hazard sites) #7615), so its stated blocker may no longer hold.
    3. Clone loop-shape coverage: loop_single_counted 13.2x and
      loop_nonzero_start 7.0x, both one matcher rule each
      (stmt/loops.rs:2382, :2495), both flat since the baseline.
    4. fields_eight 11.8x — the bounded 1..4-field descriptor rejects at 5+.
    5. Polymorphic receivers: shape_two 4.5x / shape_four 3.9x / shape_eight
      3.5x after perf: settle eight-shape static write PICs #8026.

    Two housekeeping items for whoever picks this up:

    • The INLINE_SLOT_FLOOR invariant in the body is false (see §4); no cell
      was re-measured after perf(runtime): right-size small objects — INLINE_SLOT_FLOOR 4 -> 2 (#7916, #7714) #7928 halved it.
    • docs/object-write-matrix.md needs a refresh. Its numbers are from
      8bda0351e against Node 26.3.0 at ambient load ~7; its prose still calls
      overflow-slot writes catastrophic directly above a table where they read 0.26;
      its raw CSV is not committed; and the w-cell harness it describes was never
      committed, so only the 26-cell dimension matrix above is reproducible.
  12. proggeramlug commented on Aug 14, 2026

    @proggeramlug
    ContributorAuthor

    Filed the largest of the remaining rows as #8098 — JSON.parse receivers carry class_id == 0, which rejects them from both the whole-loop clone and the static write PIC (55x at an identical write count and checksum). It is sequenced behind #8067 rather than racing it, since the fix is to give parsed objects a real ShapeId, not to relax the class_id != 0 guards.

  13. proggeramlug commented on Aug 14, 2026

    @proggeramlug
    ContributorAuthor

    Second successor filed: #8108 — slice A (root the PutValue reference across a safepointing RHS), which covers rhs_call 18.3x, rhs_pointer 9.2x and rhs_allocating 4.6x in one mechanism. Its stated blocker ("until PutValue reference temporaries have dedicated GC roots") predates the Layer-1 rooting-by-construction migration (#7615), so the first step is to re-test the premise rather than design around it.

    I am deliberately leaving this issue open. The measurement is decisive that it is not finished: 12 of 26 cells still lose to Node, and four of them (loop_single_counted 13.2x, fields_eight 11.8x, loop_nonzero_start 7.0x, key_alternating_dynamic 3.5x) are statistically flat against the 2026-07-23 baseline. Those are unfinished, not justified, which is the third clause of this issue's own closing condition. With #8098 and #8108 carrying the two biggest families, what remains here is the clone's loop-shape and field-count coverage plus polymorphic receivers (shape_two 4.5x / shape_four 3.9x / shape_eight 3.5x) — a reasonable scope for this ticket to be narrowed to, if you'd rather re-title it than close it.

  14. proggeramlug commented on Aug 18, 2026

    @proggeramlug
    ContributorAuthor

    Closing this umbrella after the object-write implementation series and the final #6821 review follow-up merged. The two largest residual families were split into focused successors and are complete: #8098 (JSON.parse receivers, landed via #8118) and #8108 (safepointing RHS, landed via #8183). The maintained matrix and historical measurements remain in-repo; any further loop-shape, field-count, or polymorphism work should use narrowly scoped follow-up issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performancetriagedMaintainer reviewed; type, scope, and next step are clear

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions