Skip to content

perf(codegen): #6812 slice A — the static write PIC still rejects any safepointing RHS (rhs_call 18x, rhs_pointer 9x, rhs_allocating 4.6x) #8108

Description

@proggeramlug

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions