Repository navigation
perf(codegen): the static write PIC pays 3 unconditional GC bookkeeping calls per write (118 instr vs the dyn IC's 22) #8184
Description
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:
pointer_testedusesjs_gc_note_slot_layout, not the_awarevariant, 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, becauseGC_OBJ_TYPED_LAYOUT_INTACTis in
WRITE_PIC_BLOCKING_FLAGSso a PIC hit implies the typed layout is already
downgraded. State it in the PR; do not inherit it silently.- The block-splitting emitter takes
ctx, not&mut LlBlock, soput.pic.hit's
hit_end_labelmust be recomputed after it (the merge phi's predecessor becomes the
emitter'sdoneblock).
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).
Activity
perf(codegen): put the static write PIC's GC bookkeeping behind one l…