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
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.
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_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.
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).
Summary
The static write PIC's hit block emits three unconditional
gc-leafbookkeepingcalls 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 decidesthe 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'sput.pic.hitblock:pointer_possibleis a compile-time claim about aType::Anyexpression, so it istruefor everyo.x = vwhose RHS is an untyped local — which in practice means mostof 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 anopaque
any:o.x = produce(r, i)const v = produce(r, i); o.x = vpointer_possible == trueSame numbers with a real
classreceiver (5.389G / 6.467G), so this is not the #8098class_id == 0shape.An
any-typed local that holds a number is the discriminating case:rhs_numericalready takes the bare arm via
is_numeric_expr, and a value that really is a pointermust 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 = vis not an exotic spelling.Proposed change
Route the
pointer_possiblearm throughemit_jsvalue_slot_store_pointer_testedinstead of
emit_jsvalue_slot_store_scalar_aware_on_block. It already emits theemit_may_carry_heap_pointer_checkpredicate, the conforming-layout skip and theemit_parent_may_need_remembering_checkgate, and it is already shipped on theclass-field store path.
Two things a PR must establish rather than assume:
pointer_testedusesjs_gc_note_slot_layout, not the_awarevariant, and skipsthe 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_INTACTis inWRITE_PIC_BLOCKING_FLAGSso a PIC hit implies the typed layout is alreadydowngraded. State it in the PR; do not inherit it silently.
ctx, not&mut LlBlock, soput.pic.hit'shit_end_labelmust be recomputed after it (the merge phi's predecessor becomes theemitter's
doneblock).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-spaceprotection — 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).