Skip to content

vm: bytecode invoke is ~1.7x costlier since #645 (explicit frame chain) #700

Description

@mparrett

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions