You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#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:
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.
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:
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.
Activity
proggeramlug commentedon Aug 20, 2026
Implemented in PR #8487: #8487
I instrumented
js_string_appendbefore changing behavior. The proposed refcount failure was not the hot shape:iso_misssamples were overwhelmingly the first append to the shared empty string (dest_blen=0,capacity=0,refcount=0); frames with a second name then reachedrefcount=1,capacity=32and used the intended in-place path.The common one-name frame was instead building the complete
[name]suffix withjs_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 -lsamples per cell, exact static-wrapper build andPERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1:iso_missThis 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;
perry1,008 passed; codegen 1,113 passed; all 52 lint gates passed; exact release build passed.