Repository navigation
perf: churn loses 1.16x to Node while executing FEWER instructions (0.99 ratio, lowest IPC in the corpus) #8408
Description
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:
- Get cache-miss and stall data (
/usr/bin/time -lreports page faults; for L1/L2/LLC misses
usexctrace/Instruments' Counters template, or a Linux run underperf statin the
colimaarm64 VM if that is easier — see the notes referenced in CLAUDE.md for the
aarch64-Linux repro path). - 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. - 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>/releaseThe 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 RSSProfiling: 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
- Correctness first: all 19 corpus programs must stay byte-exact against their Node oracle.
- Report wall time AND instructions retired AND peak RSS together. The project rule is
minimize RSS and keep best compute — never trade one for the other silently. cargo test --release -p perry-runtime --lib,-p perry --bin perry, and
bash scripts/run_lint_gates.sh(all 50) must pass.- A diff-read is a hypothesis, not a result — measure before proposing. (fix(async_hooks): track scheduler resource lifecycles #8328 looked
obviously guilty on this exact corpus and measured +0.2%.) - Known unrelated flake:
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.
Activity