Skip to content

perf: per-allocation GC layout bookkeeping is 20.1% of cycles self time and ~21% of interp #8405

Description

@proggeramlug

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.rsinit_typed_shape_layout
  • crates/perry-runtime/src/gc/layout_tables.rslayout_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions