Skip to content

perf: #8417 regressed iso_miss +24.8% (+27.2% instructions) while fixing #8394's quadratic concat #8486

Description

@proggeramlug

#8417 fixed #8394's quadratic concat but regressed iso_miss by 24.8%

Bisected on the quiet bench mini sweep. Clean adjacent-commit attribution:

commit iso_miss instructions
ddc1cd845 (parent) 11.54 G
5f9aa1f40 — #8417 "retain string accumulators in concat chains" 14.68 G

That is +27.2% instructions, and on the mini it shows as wall time 0.6810 s -> 0.8501 s
(+24.8%), pushing iso_miss from 1.96x to 2.53x Node. The sample ranges are fully
disjoint (previous max 0.7080 < current min 0.8501), so this is not noise.

-Os (#8457) is not the cause — forcing PERRY_LL_SIZE_OPT=0 gives +0.8% with
overlapping samples, and both arms already sit at ~14.7 G.

This is a real trade, not a mistake

#8417 did exactly what it claimed. Re-measured on the same fixture from #8394:

n before #8417 after #8417 Node
2000 17 ms 3 ms 1 ms
16000 1450 ms 16 ms 1 ms

90x at n=16000, and the curve went from roughly quadratic to flat. That is a large,
real win on accumulator-shaped concatenation.

The problem is that iso_miss builds a string it usually throws away — its hot line is

seen = seen + "[" + names[i] + "]";   // iso_miss.ts:205, per name per env frame

and the result is only read on a lookup miss. Retaining the accumulator helps when the
string is kept and hurts when it is discarded. iso_miss is the discard case, and it is the
row where string concat was already ~18% of self time.

What would resolve it

Ideally the retention becomes conditional rather than unconditional — retain when the
accumulator is subsequently read, fall back when it is dead. Whether that is cheaply
decidable in literals_vars.rs is the open question; if it is not, this is a genuine
policy choice about which shape to favour and should be recorded as one.

Note #8417 is small (crates/perry-codegen/src/expr/literals_vars.rs, +42/-1 plus tests),
so the surface to reason about is narrow.

Process note

I merged #8417 after validating it against the #8394 scaling fixture and confirming 19/19
corpus byte-exactness — but I did not run a timing A/B across the corpus, only a
correctness one. A perf PR that changes codegen for a common operation needs the corpus
timed, not just diffed. That gap is why this took a full sweep to surface.

Activity

  1. proggeramlug commented on Aug 20, 2026

    @proggeramlug
    ContributorAuthor

    Implemented in PR #8487: #8487

    I instrumented js_string_append before changing behavior. The proposed refcount failure was not the hot shape: iso_miss samples were overwhelmingly the first append to the shared empty string (dest_blen=0, capacity=0, refcount=0); frames with a second name then reached refcount=1, capacity=32 and used the intended in-place path.

    The common one-name frame was instead building the complete [name] suffix with js_string_concat_chain, then rooting, allocating, and copying that suffix again merely to compute "" + suffix. The fix keeps #8417's lowering and reuses the suffix only when the destination is empty and the suffix is already shared. Unique sources still copy, so the change cannot create an untracked mutable alias. Tag-checked heap-string append arms use a narrow entry point that skips repeated generic pointer validation; all non-identity cases delegate to the existing append helper.

    Three /usr/bin/time -l samples per cell, exact static-wrapper build and PERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1:

    workload metric before after result
    iso_miss instructions, median 14.790 G 12.471 G -15.7%
    wall, median (range) 0.89 s (0.89-1.44) 0.75 s (0.74-1.18) ranges overlap; no wall claim
    peak RSS, median 31.28 MiB 31.19 MiB -0.3%
    #8394 fixture instructions, median 27.063 M 27.023 M -0.15% / flat
    wall, median (range) 0.00 s (0.00-0.37) 0.00 s (0.00-0.72) timer floor; no wall claim
    peak RSS, median 5.23 MiB 5.23 MiB flat

    This recovers about 70% of the established 11.54 G -> 14.68 G regression gap while preserving #8394's asymptotic win. The residual is the extra tag-dispatch/append stage retained by #8417; closing it completely would require a fused append-chain lowering, not another safe redundant-copy elimination.

    The explicit #8394 2k/4k/8k/16k scaling probe remains 0/0/1/0 ms with correct lengths, versus the old quadratic 17/56/342/1450 ms curve.

    Validation: 19/19 sweep programs byte-exact; runtime 2,606 passed / 4 ignored; perry 1,008 passed; codegen 1,113 passed; all 52 lint gates passed; exact release build passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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