Skip to content

probe(vm): revert #645 to measure the explicit frame chain's cost - #691

Closed
mparrett wants to merge 1 commit into
mainfrom
probe/measure-645-frame-chain
Closed

probe(vm): revert #645 to measure the explicit frame chain's cost#691
mparrett wants to merge 1 commit into
mainfrom
probe/measure-645-frame-chain

Conversation

@mparrett

@mparrett mparrett commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Measurement probe. Do not merge. This reverts #645 so the perf-repeat lane can A/B main against main-minus-#645 on a single runner. It will be closed once the numbers are in.

What it measures

some over (range 1000000) is 1.38–1.49x slower on main than before #645 — 60.5 → 83.7 ms locally, holding at 10x the element count — with allocation flat at 29 MB vs 33 MB, so the cost is CPU in the call path rather than garbage. It is clean at #639 (1.03x), which rules out the primitive hoist. some's two implementations are byte-identical in logic, so nothing in the body accounts for it.

The main CPU profile puts (*Frame).runLoop, runtime.(*_panic).nextDefer and runtime.tryDeferToSpanScan in the hot path, which is what points at this commit.

Read FuncInvoke, FrameDispatch and FrameAlloc. Head faster than base by X% means #645 costs X%.

Why it is worth a lane

If it lands, one commit explains a spread of unattributed slowdowns. On main against pre-#639, get is 1.18x, nth 1.17x, str 1.11x, conj and deref 1.06x, none of which has a fast-path story of its own, and #686 leaves a 1.20x residual on reduce after restoring the fast paths it lost. A per-invoke cost in the VM would cover all of it.

Local A/B is 3 warmup / 12 runs on darwin/arm64, hyperfine, both orders:

build some over (range 1000000)
c7d3da31 (pre-#645) 60.5 ms
7c5a992a (#645) 83.7 ms
main 88.8 ms

Filed against main rather than measured locally because the laptop that produced those numbers reached load 15 mid-session and inflated a 54 ms probe to 187 ms. The interleaved same-runner lane is the instrument that does not have that failure mode.

MEASUREMENT PROBE — do not merge. This exists so the perf-repeat lane can A/B
main against main-minus-#645 on one runner.

`some` over (range 1000000) is 1.38-1.49x slower on main than before #645
(60.5 -> 83.7 ms local, holding at 10x scale) with allocation flat, so the cost
is CPU in the call path, not garbage. It is clean at #639, which rules out the
primitive hoist. The tip CPU profile puts (*Frame).runLoop, runtime.(*_panic).
nextDefer and runtime.tryDeferToSpanScan in the hot path, which points at this
commit. The same per-invoke cost plausibly explains why `get`, `nth`, `str`,
`conj` and `deref` all sit 1.06-1.18x above pre-#639 with no fast-path story,
and the residual left after #686.

Head faster than base by X% here means #645 costs X%. FuncInvoke, FrameDispatch
and FrameAlloc are the families to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mparrett mparrett added the perf-repeat Run the repeat A/B (variance-reduced) perf check label Aug 6, 2026
@mparrett

mparrett commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Numbers are in, so this probe has done its job and I am closing it.

Result

perf-repeat, N=7 interleaved ABBA on one runner, 53/53 families comparable, none flaky or missing. Negative means this branch (the revert) is faster, so the cost belongs to #645:

family Δ
FuncInvoke/Direct −41.8%
FuncInvoke/Closure −41.7%
MultiArity/2Args −39.9%
MultiArity/1Arg −39.3%
VariadicInvoke/1Arg −24.1%
FrameDispatch −14.0%
VariadicInvoke/5Args −13.7%
FrameAlloc/3Args −9.6%
FrameAlloc/NoArgs −9.2%
SeqIteration/List/100 +13.3%
SeqIteration/List/10 +10.9%

FuncInvoke/Native flat at +0.31%; median across all 53 families −0.39%. The SeqIteration/List pair is the win #645 bought and the only would-gate hit in either direction.

The capability stays

At 1M levels of non-tail recursion main returns a result and this branch dies with fatal error: stack overflow, which is what #644 exists to prevent. #620 is also designed on top of the explicit-frame VM, so the shape stays.

Follow-up analysis — which defers were priced and ruled out, where the remaining cost sits, and why the FuncInvoke micro misleads on this path — is in #644.

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

Labels

perf-repeat Run the repeat A/B (variance-reduced) perf check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant