Splitting this out of #656, which is otherwise closed by #686. The reduce regression there had two causes stacked on each other: the shadowed registration (fixed) and this, which is shared with a spread of other primitives.
#645 replaced the Go-recursive Frame.Run descent with an explicit frame chain. That bought real things — see the trade below — but it made bytecode function invocation roughly 1.7x more expensive.
Measurement
A revert of #645 was pushed as draft PR #691 and labeled perf-repeat, so the interleaved same-runner lane did the A/B: N=7 ABBA, EPYC 9V45, 53/53 families comparable, none flaky. Negative delta means the revert is faster, so the cost belongs to #645.
| family |
delta on revert |
FuncInvoke/Direct |
−41.8% (main = 1.73x) |
FuncInvoke/Closure |
−41.7% |
MultiArity/1Arg, 2Args |
~−39% |
VariadicInvoke/1Arg |
−24.1% |
FrameDispatch |
−14.0% |
FrameAlloc/NoArgs, 3Args |
−9.2%, −9.6% |
SeqIteration/List/{10,100} |
+13.3% |
FuncInvoke/Native is flat at +0.31%, which is the coherence check: #645 touched the bytecode call path, so native invoke should be untouched, and is.
What it explains
These primitives sit above their pre-#639 timings with no fast-path story of their own, and the cause is here rather than in #639's primitive hoist: some 1.49x, get 1.18x, nth 1.17x, str 1.11x, conj and deref 1.06x. It is also the ~1.20x still on reduce after #686 restored its fast paths.
some is the clearest case: clean at #639 (1.03x), its two implementations byte-identical in logic, and its main CPU profile puts (*Frame).runLoop, runtime.(*_panic).nextDefer and runtime.tryDeferToSpanScan in the hot path.
The trade
A revert gives back both of #645's gains: the SeqIteration/List figure above, and the non-recursive descent, which is what keeps deep lg→lg recursion from aborting the process (#644).
The open question is whether the per-invoke cost is inherent to the explicit chain or an artifact of how the frame transition is currently built. The defer-related entries in the profile point at the latter, which is worth checking before treating the cost as fixed.
#646 is adjacent but narrower: it tracks restoring #618's self-call shortcut on top of #645, which would help self-calls rather than the general invoke path measured here.
Note on the measurement method
The perf, perf-repeat and perf-wasm labels all read the pr-fast profile, which is one pkg/vm job with a fixed family regex. No rt-level workload (reduce, seq ops) is covered by it, and a family absent from the base commit is reported as new rather than compared. The primitive figures above were measured separately.
Splitting this out of #656, which is otherwise closed by #686. The
reduceregression there had two causes stacked on each other: the shadowed registration (fixed) and this, which is shared with a spread of other primitives.#645 replaced the Go-recursive
Frame.Rundescent with an explicit frame chain. That bought real things — see the trade below — but it made bytecode function invocation roughly 1.7x more expensive.Measurement
A revert of #645 was pushed as draft PR #691 and labeled
perf-repeat, so the interleaved same-runner lane did the A/B: N=7 ABBA, EPYC 9V45, 53/53 families comparable, none flaky. Negative delta means the revert is faster, so the cost belongs to #645.FuncInvoke/Directmain= 1.73x)FuncInvoke/ClosureMultiArity/1Arg,2ArgsVariadicInvoke/1ArgFrameDispatchFrameAlloc/NoArgs,3ArgsSeqIteration/List/{10,100}FuncInvoke/Nativeis flat at +0.31%, which is the coherence check: #645 touched the bytecode call path, so native invoke should be untouched, and is.What it explains
These primitives sit above their pre-#639 timings with no fast-path story of their own, and the cause is here rather than in #639's primitive hoist:
some1.49x,get1.18x,nth1.17x,str1.11x,conjandderef1.06x. It is also the ~1.20x still onreduceafter #686 restored its fast paths.someis the clearest case: clean at #639 (1.03x), its two implementations byte-identical in logic, and itsmainCPU profile puts(*Frame).runLoop,runtime.(*_panic).nextDeferandruntime.tryDeferToSpanScanin the hot path.The trade
A revert gives back both of #645's gains: the
SeqIteration/Listfigure above, and the non-recursive descent, which is what keeps deep lg→lg recursion from aborting the process (#644).The open question is whether the per-invoke cost is inherent to the explicit chain or an artifact of how the frame transition is currently built. The defer-related entries in the profile point at the latter, which is worth checking before treating the cost as fixed.
#646 is adjacent but narrower: it tracks restoring #618's self-call shortcut on top of #645, which would help self-calls rather than the general invoke path measured here.
Note on the measurement method
The
perf,perf-repeatandperf-wasmlabels all read thepr-fastprofile, which is onepkg/vmjob with a fixed family regex. No rt-level workload (reduce, seq ops) is covered by it, and a family absent from the base commit is reported as new rather than compared. The primitive figures above were measured separately.