Skip to content

perf: shapes 3x instruction-count regression (also cycles, deeplist) between 73df2a6e7 and 905017b1c #8648

Description

@proggeramlug

Three sweep-corpus benchmarks regressed between 73df2a6e7 and 905017b1c (the nine commits landed on 2026-08-23). shapes is 3× the instruction count.

Evidence

Reproduced on two machines with two different methods, so this is not bench-host noise.

Bench mini, perry/node ratio (same scriptc/porffor/node pins, two independent runs):

bench baseline 73df2a6e7 run 1 run 2
shapes 0.931x 2.917x 2.912x
cycles 0.842x 1.331x 1.363x
deeplist 0.474x 0.569x 0.580x
pipeline 0.919x 1.114x 1.083x

The other 16 rows sit on baseline in both runs. Both runs were flagged DIRTY on load, but contamination picks different victims each time — these reproduce to within ~2%, so the load flag is not the explanation.

Local Mac, wall clock, best-of-3 on the compiled binaries directly:

shapes     old=0.09s  new=0.38s   4.22x SLOWER
cycles     old=0.07s  new=0.12s   1.71x SLOWER
deeplist   old=0.05s  new=0.06s   1.20x SLOWER
fib40      old=0.65s  new=0.52s   0.80x (faster)
tree       old=0.30s  new=0.28s   0.93x (fine)

It is extra mutator work, not GC and not scheduling (/usr/bin/time -l, p_shapes):

old new
instructions retired 1,186,944,293 3,667,840,774 3.09x
cycles elapsed 319,225,659 970,790,835 3.04x
max RSS 33.5 MB 46.0 MB 1.37x
GC diag lines (PERRY_GC_DIAG=1) 26 26 identical

Same instruction/cycle ratio either side, identical collection count — the program simply executes ~2.5 billion more instructions. That is the shape of a per-operation linear scan being added to a hot path.

What it is not

Leading suspect

shapes is by far the most class-heavy row in the corpus (7 classes, inheritance, super() chains, static members, getters, instanceof, virtual dispatch), and severity tracks class complexity rather than mere class presence — tree, tree_wide and push_cls each use one class and are unaffected.

That points at #8630 (class semantics tail, landed 905017b1c). Its receiver_has_own_key guard added to set_field_by_name_object_tail is a keys-array scan on the by-name store path, which is exactly a per-operation linear scan. The #8630 author already noticed the cost and moved own_key_present to be evaluated last and only on the slow path — the question is whether class-instance stores (this.x = x in a constructor, run 120,000 times here) still reach it.

Candidate window, most to least likely: #8630 (class semantics) > #8638 (node:test) > #8632/#8637 (HTTP) > #8633/#8623 (spill, largely excluded above).

Not yet done

A proper bisect. I built 00bddb34b (immediately pre-#8630) to A/B it, but the build was interrupted and the host is at load ~45, so I stopped rather than produce a measurement I could not trust. The decisive next step is a perry built at 00bddb34b timed against 905017b1c on shapes.

Repro:

# old binary is preserved in the sweep artifacts
./sweep-artifacts-0822e/perry/p_shapes     # 73df2a6e7
./sweep-artifacts-0823b/perry/p_shapes     # 905017b1c
/usr/bin/time -l ./…/p_shapes 2>&1 | grep -E "instructions retired|cycles elapsed"

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