Fresh leaf profile of push_cls on v0.5.1325 — the construction picture has changed
The #7469 decomposition in docs/engine-plan.md is from the v0.5.1299 era and
its top rows are now closed. Re-profiled on current main (v0.5.1325,
f3d290874, own --release build, PERRY_DEBUG_SYMBOLS=1), two independent
sample runs:
| item |
run 1 (766 leaf) |
run 2 (379 leaf) |
gc::layout::typed_shape_layout_entry |
26.4% |
24.8% |
write barriers — js_write_barrier_slot + write_barrier_decoded_parent + barrier_child_prologue + incremental_mark_barrier_value |
25.0% |
26.9% |
user code — chunk + Node_constructor |
23.9% |
20.8% |
js_array_length |
10.2% |
15.0% |
js_ctor_return_override + constructor_return_overrides_this |
4.0% |
~3% |
is_registered_set + is_registered_map |
3.1% |
— |
gc::layout_tables::layout_forget_object |
1.7% |
2.9% |
_tlv_get_addr |
1.0% |
— |
Confirms the last campaign worked. _tlv_get_addr is 1.0%, from 27.0%
(#7565). layout_forget_object is 1.7–2.9%, from 14.5% (#7525/#7532).
Neither is a lever any more.
Method, stated plainly. Dev Mac under load ~8, not the pinned quiet mini, so
these are leaf-share figures and not publishable wall-clock. A share
decomposition is far more robust to external load than a timing is — the process
executes the same instructions either way — and the two runs agree within a few
points on every row. Re-measure on perry-macos.local before quoting absolute
times.
Two levers, and one surprise
1. typed_shape_layout_entry ~25% — the top single function
push_cls's Node takes the js_gc_declare_typed_shape_layout path
(verified in the emitted IR: 1 call to declare, 0 to init), so this is not
the ValidateSlots loop — #7515/#7532 are working as intended. It is the install
itself, which layout.rs:1022 documents as reducing, on a shape_install::hit,
to "the two header bit-writes shape_install_shared would have performed".
If that is what it costs on the hit path, why is it an FFI call at all? The
emitted call is
call void @js_gc_declare_typed_shape_layout(i64 %r55, i32 2,
ptr @perry_typed_shape_raw_f64_mask_push_cls_ts__Node, i32 1, ptr null, i32 0)
— every argument but the object pointer is a compile-time constant for the
class. This is the exact shape #7566 just won 1.81× on: an FFI call whose real
cost is the call plus the thread-local resolutions it performs, where the work
itself is a couple of stores. The same remedy may apply (emit the hit path
inline, keep the call as the slow path), with the same size discipline —
#7566's loop-membership gate is the precedent for not paying bytes at every
site.
2. Write barriers ~25% — now tied for the top spot
Owned by #7511, and correctness-first: a missed barrier is a use-after-free,
not a slowdown. Noting only that it is no longer 16.1% — as a share it has grown
because everything around it shrank. PERRY_WRITE_BARRIERS=0 cannot be used to
bound it (it also switches the collector out of evacuating mode and makes the
bench slower).
3. js_array_length at 10–15% — the surprise
push_cls's inner loop is keep.push(new Node(v, w)). That an array-length read
is 10–15% of the profile is hard to justify: the length is a field of the
ArrayHeader the push already has in hand, and js_array_length is in the
checker's NONCOLLECTING set precisely because it is a plain accessor
(array/indexing.rs:537). This looks like one FFI call per push for a load that
should fold, and it is the cheapest of the three to investigate.
Not levers, but odd
is_registered_set + is_registered_map at ~3% in a benchmark containing
no Set and no Map. That is the shape #7510 already hit once — one immortal
side-table entry nullifying an is_empty() fast path — and worth a look while
someone is in this code.
Fresh leaf profile of
push_clson v0.5.1325 — the construction picture has changedThe
#7469decomposition indocs/engine-plan.mdis from the v0.5.1299 era andits top rows are now closed. Re-profiled on current
main(v0.5.1325,f3d290874, own--releasebuild,PERRY_DEBUG_SYMBOLS=1), two independentsampleruns:gc::layout::typed_shape_layout_entryjs_write_barrier_slot+write_barrier_decoded_parent+barrier_child_prologue+incremental_mark_barrier_valuechunk+Node_constructorjs_array_lengthjs_ctor_return_override+constructor_return_overrides_thisis_registered_set+is_registered_mapgc::layout_tables::layout_forget_object_tlv_get_addrConfirms the last campaign worked.
_tlv_get_addris 1.0%, from 27.0%(#7565).
layout_forget_objectis 1.7–2.9%, from 14.5% (#7525/#7532).Neither is a lever any more.
Method, stated plainly. Dev Mac under load ~8, not the pinned quiet mini, so
these are leaf-share figures and not publishable wall-clock. A share
decomposition is far more robust to external load than a timing is — the process
executes the same instructions either way — and the two runs agree within a few
points on every row. Re-measure on
perry-macos.localbefore quoting absolutetimes.
Two levers, and one surprise
1.
typed_shape_layout_entry~25% — the top single functionpush_cls'sNodetakes thejs_gc_declare_typed_shape_layoutpath(verified in the emitted IR: 1 call to
declare, 0 toinit), so this is notthe
ValidateSlotsloop — #7515/#7532 are working as intended. It is the installitself, which
layout.rs:1022documents as reducing, on ashape_install::hit,to "the two header bit-writes
shape_install_sharedwould have performed".If that is what it costs on the hit path, why is it an FFI call at all? The
emitted call is
— every argument but the object pointer is a compile-time constant for the
class. This is the exact shape #7566 just won 1.81× on: an FFI call whose real
cost is the call plus the thread-local resolutions it performs, where the work
itself is a couple of stores. The same remedy may apply (emit the hit path
inline, keep the call as the slow path), with the same size discipline —
#7566's loop-membership gate is the precedent for not paying bytes at every
site.
2. Write barriers ~25% — now tied for the top spot
Owned by #7511, and correctness-first: a missed barrier is a use-after-free,
not a slowdown. Noting only that it is no longer 16.1% — as a share it has grown
because everything around it shrank.
PERRY_WRITE_BARRIERS=0cannot be used tobound it (it also switches the collector out of evacuating mode and makes the
bench slower).
3.
js_array_lengthat 10–15% — the surprisepush_cls's inner loop iskeep.push(new Node(v, w)). That an array-length readis 10–15% of the profile is hard to justify: the length is a field of the
ArrayHeaderthe push already has in hand, andjs_array_lengthis in thechecker's
NONCOLLECTINGset precisely because it is a plain accessor(
array/indexing.rs:537). This looks like one FFI call per push for a load thatshould fold, and it is the cheapest of the three to investigate.
Not levers, but odd
is_registered_set+is_registered_mapat ~3% in a benchmark containingno Set and no Map. That is the shape #7510 already hit once — one immortal
side-table entry nullifying an
is_empty()fast path — and worth a look whilesomeone is in this code.