Skip to content

perf: churn loses 1.16x to Node while executing FEWER instructions (0.99 ratio, lowest IPC in the corpus) #8408

Description

@proggeramlug

Summary

churn is the only losing row that already executes fewer instructions than Node and
still loses. Instruction ratio 0.99, yet it runs at 1.16x Node. Its IPC is the
lowest in the entire corpus — 13.35 G instr/s against ~19 typical for Perry rows. This
is a memory-stall / locality problem, and no amount of instruction removal will fix it.

Evidence

Measured on 8bfdac447:

Perry Node
wall 0.1987 s 0.1710 s
instructions 2.65 G 2.68 G
peak RSS 20.9 MiB 85.7 MiB

So Perry does slightly less work, in a quarter of the memory, and still takes 16% longer.

Self-time profile (symbols kept, 14 runs aggregated):

 92.4%  perry_fn_churn_ts__chunk$spec_b            <- user code, one function
  3.6%  js_string_addref_if_heap_string
  3.2%  value::addr_class::try_read_tracked_gc_header
  0.5%  _platform_memmove
  0.3%  gc::layout::layout_transfer

There is no runtime hot spot to remove — 92.4% is inside one compiled user function. The
cost is how that function's data is laid out and traversed, not how many instructions it runs.

What to investigate

This one needs hardware counters, not a sampling profiler. Suggested approach:

  1. Get cache-miss and stall data (/usr/bin/time -l reports page faults; for L1/L2/LLC misses
    use xctrace/Instruments' Counters template, or a Linux run under perf stat in the
    colima arm64 VM if that is easier — see the notes referenced in CLAUDE.md for the
    aarch64-Linux repro path).
  2. Compare Perry's object layout for the benchmark's hot structure against what V8 does.
    Relevant known facts: a 2-field object literal is 72 bytes in Perry, and the GC header is
    40 bytes since perf(object): shrink common objects to 40 bytes #8313/perf(gc, codegen): recover the instruction cost of the 56 B → 48 B header shrink (#8122) #8204. Node reaches 85.7 MiB peak RSS here versus Perry's 20.9 MiB,
    so Perry is denser — which makes the stall pattern more interesting, not less. It may be
    allocation ordering / traversal order rather than object size.
  3. Check whether nursery allocation order matches traversal order for this workload, and
    whether a copying minor is reordering survivors in a way that hurts locality.

churn's source is sweep-artifacts-0819b/sources/churn.ts; the hot function is chunk.

Why it matters beyond one row

churn_alloc (0.78x) and push_cls (0.80x) are the same workload family and already win, so
whatever churn does differently is isolatable by diffing it against those two. That is
probably the cheapest first step: find the one structural difference between churn and
churn_alloc that costs 16%.

Acceptance

churn beats Node (< 1.00x), with a stated mechanism — a fix that moves the number without
an explanation of why will not survive the next layout change. 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