Summary
Per-allocation GC layout bookkeeping is the single largest cross-cutting runtime cost in the
benchmark corpus. It is 20.1% of self time on cycles, and appears on every
allocation-heavy row. Removing or shrinking it would flip at least one row from losing to
winning against Node and materially narrow two more.
Profile evidence
Measured on 8bfdac447, symbols kept, self time aggregated over 14 runs each.
cycles (currently 1.01x Node — the closest losing row; instruction ratio 2.51):
51.8% perry_fn_cycles_ts__makeCycle$spec_i32 <- user code
28.1% cycles_ts__Cell_constructor <- user code
20.1% gc::layout::typed_shape::init_typed_shape_layout <- THIS
cycles loses by 1%. A 20.1% self-time item is far more than enough to flip it.
The same family shows up across the corpus:
| row |
P/Node |
layout/alloc bookkeeping in self time |
cycles |
1.01 |
init_typed_shape_layout 20.1% |
interp |
1.55 |
init_typed_shape_layout 6.6%, js_inline_arena_state 6.2%, layout_note_slot 4.0%, immortal_layout_scope_active 2.2%, layout_forget_object 1.8% — ~20.8% combined |
iso_miss |
2.02 |
init_typed_shape_layout 4.5%, js_inline_arena_state 5.3%, layout_note_slot 1.2% |
pipeline |
1.26 |
layout_forget_object 1.5% |
Where it lives
crates/perry-runtime/src/gc/layout/typed_shape.rs — init_typed_shape_layout
crates/perry-runtime/src/gc/layout_tables.rs — layout_note_slot,
layout_forget_object, immortal_layout_scope_active
js_inline_arena_state
Note this code is already optimized once: #7578 removed a duplicated
gc_type_layout_slot_kind lookup, and #8289 added a ShapeId memo so a proven tuple replays
without rehashing the ShapeTable. So the easy wins are gone — this needs a structural idea,
not another micro-tweak. Worth asking:
- Can the layout for a given shape be computed once per shape and stamped by reference,
rather than re-derived per object?
- Can the whole call be skipped for objects whose type is statically known at the allocation
site, with codegen emitting the layout inline?
- Is the
ObjectFields slot-kind check on the hot path when it could be hoisted?
Acceptance
cycles beats Node (< 1.00x) with interp and iso_miss improved and no row regressed,
all 19 rows byte-exact, RSS not increased.
How to build and measure (shared setup)
Build with the static wrappers or you link a stale runtime and both A/B arms behave
identically — a vacuous result:
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
export PERRY_RUNTIME_DIR=<your target>/release
The benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/ (sources/*.ts, expected/*.stdout).
export PERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1
perry sources/<row>.ts -o /tmp/x --no-cache
cmp <(/tmp/x) expected/<row>.stdout # verify BEFORE timing
/usr/bin/time -l /tmp/x # instructions retired + peak RSS
Profiling: Perry strips its output binaries, so sample attributes everything to
???. Set PERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1 at compile time — the binary keeps
~1900 symbols and stays byte-exact — then read sample's "Sort by top of stack" section.
These programs are short; aggregate across ~14 runs for a usable sample count.
Constraints that apply to any fix here
Summary
Per-allocation GC layout bookkeeping is the single largest cross-cutting runtime cost in the
benchmark corpus. It is 20.1% of self time on
cycles, and appears on everyallocation-heavy row. Removing or shrinking it would flip at least one row from losing to
winning against Node and materially narrow two more.
Profile evidence
Measured on
8bfdac447, symbols kept, self time aggregated over 14 runs each.cycles(currently 1.01x Node — the closest losing row; instruction ratio 2.51):cyclesloses by 1%. A 20.1% self-time item is far more than enough to flip it.The same family shows up across the corpus:
cyclesinit_typed_shape_layout20.1%interpinit_typed_shape_layout6.6%,js_inline_arena_state6.2%,layout_note_slot4.0%,immortal_layout_scope_active2.2%,layout_forget_object1.8% — ~20.8% combinediso_missinit_typed_shape_layout4.5%,js_inline_arena_state5.3%,layout_note_slot1.2%pipelinelayout_forget_object1.5%Where it lives
crates/perry-runtime/src/gc/layout/typed_shape.rs—init_typed_shape_layoutcrates/perry-runtime/src/gc/layout_tables.rs—layout_note_slot,layout_forget_object,immortal_layout_scope_activejs_inline_arena_stateNote this code is already optimized once: #7578 removed a duplicated
gc_type_layout_slot_kindlookup, and #8289 added a ShapeId memo so a proven tuple replayswithout rehashing the ShapeTable. So the easy wins are gone — this needs a structural idea,
not another micro-tweak. Worth asking:
rather than re-derived per object?
site, with codegen emitting the layout inline?
ObjectFieldsslot-kind check on the hot path when it could be hoisted?Acceptance
cyclesbeats Node (< 1.00x) withinterpandiso_missimproved and no row regressed,all 19 rows byte-exact, RSS not increased.
How to build and measure (shared setup)
Build with the static wrappers or you link a stale runtime and both A/B arms behave
identically — a vacuous result:
The benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/(sources/*.ts,expected/*.stdout).Profiling: Perry strips its output binaries, so
sampleattributes everything to???. SetPERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1at compile time — the binary keeps~1900 symbols and stays byte-exact — then read
sample's "Sort by top of stack" section.These programs are short; aggregate across ~14 runs for a usable sample count.
Constraints that apply to any fix here
minimize RSS and keep best compute — never trade one for the other silently.
cargo test --release -p perry-runtime --lib,-p perry --bin perry, andbash scripts/run_lint_gates.sh(all 50) must pass.obviously guilty on this exact corpus and measured +0.2%.)
box::release_tests::the_intrusive_free_list_round_trips_a_whole_cohort(flaky: the_intrusive_free_list_round_trips_a_whole_cohort asserts a delta on a process-global counter under parallel tests #8401). If it fails once and reruns clean, it is not yours.