Skip to content

perf: shapes spends 100% of self time in try_read_tracked_gc_header + shape_descriptor_by_id (1.06x Node) #8406

Description

@proggeramlug

Summary

100% of shapes' measured self time is two runtime helpers — no user code appears at
all. shapes currently runs at 1.06x Node with an instruction ratio of 1.25, so this is
work being executed, not scheduling.

Profile evidence

Measured on 8bfdac447, symbols kept, aggregated over 40 runs (the program is short, ~89 ms):

 67.7%  value::addr_class::try_read_tracked_gc_header
 32.3%  object::shapes::shape_descriptor_by_id

That is the entire attributed profile. The benchmark's own functions never appear above the
sampling floor.

try_read_tracked_gc_header also shows up on churn (3.2%), so this is not confined to one row.

Why this looks addressable

shapes exercises polymorphic property access across many object shapes — exactly the path
these two helpers serve. Two questions worth answering with measurements:

  1. shape_descriptor_by_id — is this a map lookup on every property access? A shape
    descriptor is immutable once created, so a per-callsite inline cache (shape id -> descriptor
    pointer, invalidated on shape transition) should turn most lookups into a compare-and-branch.
    Check whether an inline cache already exists and is simply missing here, or whether the id
    -> descriptor step is unavoidably dynamic.
  2. try_read_tracked_gc_header — being the single largest frame in a property-access
    benchmark suggests a validation read on a very hot path. Find out what it is guarding and
    whether the check can be hoisted out of the access loop, or proven redundant when the
    receiver was already validated earlier in the same operation.

Relevant background: crates/perry-runtime/src/value/addr_class.rs centralizes
handle-vs-pointer address classification (#4899), and CLAUDE.md notes that class instances
carry no separate shape word — the shape word is parent_class_id, valid only when
class_id == 0.

Where to look

  • crates/perry-runtime/src/value/addr_class.rstry_read_tracked_gc_header
  • crates/perry-runtime/src/object/shapes.rsshape_descriptor_by_id
  • the property-get/set lowering in crates/perry-codegen/src/expr/property_get/

Acceptance

shapes beats Node (< 1.00x), no other row regressed, all 19 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