Skip to content

perf(codegen): the static write PIC pays 3 unconditional GC bookkeeping calls per write (118 instr vs the dyn IC's 22) #8184

Description

@proggeramlug

Summary

The static write PIC's hit block emits three unconditional gc-leaf bookkeeping
calls
whenever the stored value is not provable non-pointer at compile time. The
tree already contains the emitter that puts exactly those three calls behind one
inline live test of the stored bits
emit_jsvalue_slot_store_pointer_tested
(#7511, expr/write_barrier.rs:649) — and the sibling dynamic-key IC already decides
the same question the same way. The static PIC does not use either.

Measured cost: 118 instructions per write versus 22 for the identical store on the
dynamic-key IC.

Where

crates/perry-codegen/src/expr/proxy_reflect.rs, lower_put_value_static_write_ic's
put.pic.hit block:

let pointer_possible = !(is_numeric_expr(ctx, value)
    || expr_produces_non_pointer_bits_by_construction(ctx, value));if pointer_possible {
    emit_jsvalue_slot_store_scalar_aware_on_block()   // 3 unconditional calls
} else {
    blk.store(DOUBLE, &stored_value, &field_ptr);      // bare
}

pointer_possible is a compile-time claim about a Type::Any expression, so it is
true for every o.x = v whose RHS is an untyped local — which in practice means most
of them. The emitted hit block then always runs
js_string_addref_if_heap_string + js_gc_note_slot_layout_aware +
js_write_barrier_slot, even when the value is a plain double at every execution.

Measurement (a3118cfea, release, best-of-5, verified against Node 26.5.1)

12M writes of a number produced by an uninlinable closure, o.x = …, receiver an
opaque any:

probe write path instructions per write
loop with the store REMOVED (call + local kept) 5.103G
o.x = produce(r, i) dynamic-key IC, live tag test, bare store 5.362G 22
const v = produce(r, i); o.x = v static write PIC, pointer_possible == true 6.512G 118

Same numbers with a real class receiver (5.389G / 6.467G), so this is not the #8098
class_id == 0 shape.

An any-typed local that holds a number is the discriminating case: rhs_numeric
already takes the bare arm via is_numeric_expr, and a value that really is a pointer
must pay the bookkeeping anyway. So the win is bounded by how often a PIC site's value
is dynamically non-pointer — but on that shape it is roughly 5x on the write itself,
and const v = f(); o.x = v is not an exotic spelling.

Proposed change

Route the pointer_possible arm through emit_jsvalue_slot_store_pointer_tested
instead of emit_jsvalue_slot_store_scalar_aware_on_block. It already emits the
emit_may_carry_heap_pointer_check predicate, the conforming-layout skip and the
emit_parent_may_need_remembering_check gate, and it is already shipped on the
class-field store path.

Two things a PR must establish rather than assume:

  1. pointer_tested uses js_gc_note_slot_layout, not the _aware variant, and skips
    the note entirely when the new value carries no pointer. That drops the clearing
    half — an old pointer overwritten by a double leaves the mask bit set, so the slot
    stays conservatively scanned. perf(gc): write barriers cost 16% on an all-numeric store workload — elide on provably-non-pointer stores #7511 argues that direction is safe ("can only ever
    CLEAR mask state, never set it"), and its precondition — requires_raw_f64 == false
    — does hold here, because GC_OBJ_TYPED_LAYOUT_INTACT is in
    WRITE_PIC_BLOCKING_FLAGS so a PIC hit implies the typed layout is already
    downgraded. State it in the PR; do not inherit it silently.
  2. The block-splitting emitter takes ctx, not &mut LlBlock, so put.pic.hit's
    hit_end_label must be recomputed after it (the merge phi's predecessor becomes the
    emitter's done block).

Not covered by any runtime probe

Recorded from #8183: a release build with the write barrier deleted from the dynamic
IC's equivalent arm passes an adversarial behavioural matrix — old→young edges, forced
evacuation with PERRY_GC_VERIFY_EVACUATION=1, PERRY_GEN_GC=0, zeal plus from-space
protection — byte-identical output, exit 0. Any PR here needs static IR assertions
over the guarded/unguarded arms plus sabotage runs; a green behavioural suite says
nothing about this class.

Found while measuring #8108's premise (see
#8108 (comment)). Split out so it
is not confused with the safepoint gate, which is a different question and is staying
put. Related: #6812 (umbrella), #7511 (the emitter), #8183 (the same fix on the
dynamic-key IC's reference arm).

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