Summary
value::dyn_index.rs runs set::is_registered_set(raw_ptr) || map::is_registered_map(raw_ptr)
on every dynamic index read and write — js_dyn_index_get (:232) and
js_dyn_index_set (:544) — ahead of the GcHeader read the same function performs
about 90 lines later. Both probes already end in exactly that header byte
(set.rs:262 → obj_type == GC_TYPE_SET, map.rs:244 → GC_TYPE_MAP), so the header
can rule both out for free. This is the same shape as #7474 / #7765 / #7850.
Measurement
Symbolicated sample of gc-handoff/apps/interp.ts (PERRY_DEBUG_SYMBOLS=1, release
runtime @ bf98134ba, 2069 main-thread samples, DispatchQueue section only):
perry_fn_interp_ts__lookup
-> js_dyn_index_get
-> set::is_registered_set 12 samples (0.58%)
plus scattered is_registered_set / is_registered_map leaves elsewhere, for roughly
1% of interp — in a program that constructs no Set and no Map.
Caveat, stated because this family has produced vacuous numbers before: this profile was
taken on a contended dev machine, so treat it as attribution, not a timing claim.
The absolute cost has to be re-measured on the quiet mini before anyone sizes a fix from
it, and this family is workload-dependent (see #7850's sizing caveat — the same probe
measured 6.5% on one program and exactly zero on another, both correctly).
Why the existing latch does not cover it
SET_REGISTRY_EVER_USED / MAP_REGISTRY_EVER_USED (#7469) do their job when idle, but
is_registered_set is a real out-of-line pub fn, so even the idle path costs a call
per dynamic index access. Whether the latches are armed on interp was not
established — establishing it is step one of any fix, because it decides whether the
win is "one call" or "a call plus a thread-local resolution plus a hash".
Care required
js_dyn_index_get deliberately probes the registries before the is_valid_obj_ptr
guard and before any header dereference, and both is_registered_set and
is_registered_map carry inline comments explaining why: probing addr - 8 for an
arbitrary above-band candidate pointer segfaults on Linux, where freed/foreign pages get
unmapped (mimalloc on macOS retains them and hides it). Any reordering has to keep that
property — addr_class::try_read_gc_header is the safe read, but it is not free either,
so measure the trade rather than assuming it.
Found while fixing #7850; not included there because it is a different function with its
own risk surface, and mixing it in would have made the A/B attribution ambiguous.
Summary
value::dyn_index.rsrunsset::is_registered_set(raw_ptr) || map::is_registered_map(raw_ptr)on every dynamic index read and write —
js_dyn_index_get(:232) andjs_dyn_index_set(:544) — ahead of theGcHeaderread the same function performsabout 90 lines later. Both probes already end in exactly that header byte
(
set.rs:262→obj_type == GC_TYPE_SET,map.rs:244→GC_TYPE_MAP), so the headercan rule both out for free. This is the same shape as #7474 / #7765 / #7850.
Measurement
Symbolicated
sampleofgc-handoff/apps/interp.ts(PERRY_DEBUG_SYMBOLS=1, releaseruntime @
bf98134ba, 2069 main-thread samples, DispatchQueue section only):plus scattered
is_registered_set/is_registered_mapleaves elsewhere, for roughly1% of
interp— in a program that constructs noSetand noMap.Caveat, stated because this family has produced vacuous numbers before: this profile was
taken on a contended dev machine, so treat it as attribution, not a timing claim.
The absolute cost has to be re-measured on the quiet mini before anyone sizes a fix from
it, and this family is workload-dependent (see #7850's sizing caveat — the same probe
measured 6.5% on one program and exactly zero on another, both correctly).
Why the existing latch does not cover it
SET_REGISTRY_EVER_USED/MAP_REGISTRY_EVER_USED(#7469) do their job when idle, butis_registered_setis a real out-of-linepub fn, so even the idle path costs a callper dynamic index access. Whether the latches are armed on
interpwas notestablished — establishing it is step one of any fix, because it decides whether the
win is "one call" or "a call plus a thread-local resolution plus a hash".
Care required
js_dyn_index_getdeliberately probes the registries before theis_valid_obj_ptrguard and before any header dereference, and both
is_registered_setandis_registered_mapcarry inline comments explaining why: probingaddr - 8for anarbitrary above-band candidate pointer segfaults on Linux, where freed/foreign pages get
unmapped (mimalloc on macOS retains them and hides it). Any reordering has to keep that
property —
addr_class::try_read_gc_headeris the safe read, but it is not free either,so measure the trade rather than assuming it.
Found while fixing #7850; not included there because it is a different function with its
own risk surface, and mixing it in would have made the A/B attribution ambiguous.