Skip to content

perf(vm,rt): add PreparedCall for per-element callback loops; adopt in some - #726

Merged
mparrett merged 3 commits into
mainfrom
perf/vm-prepared-call
Aug 12, 2026
Merged

perf(vm,rt): add PreparedCall for per-element callback loops; adopt in some#726
mparrett merged 3 commits into
mainfrom
perf/vm-prepared-call

Conversation

@mparrett

@mparrett mparrett commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

First implementation PR from #720, stacked on #719. A native seq fn calling a bytecode predicate pays three costs on every ec.Invoke that are constants of the call site, not of the call: resolveBytecodeCall (same target every element), two framePoolMu lock/unlock pairs, and full frame init/teardown. PreparedCall resolves the callable once, owns one frame, and per call resets only what a callee can have changed.

The reset covers more than args/ip/sp, and the tests pin why: a tail call in the callee body rebinds the owned frame's code/consts/ closedOvers through installBytecodeCall, an error unwind can leave stale handlers, and tracing can arm debug mid-walk. PrepareCall returns nil for variadic, native, and other non-bytecode targets, and for arities without a matching CallN entry point (Call1 is the only one here), so rt.Some keeps its reused-fargs ec.Invoke fallback and the change stays incremental. A PreparedCall is single-owner per native call activation; nested uses each prepare their own.

Adopted in rt.Some (both the chunked and linear walk arms). Measured on (some (fn [x] (< 9999999 x)) (range 1000000)), darwin/arm64, interleaved mins: 50.1 ms vs 70.9 on the base branch and ~58.4 on v1.12.2 — below the release baseline, with #644's stack-safety kept. xsofy's headless engine bench shows the same direction per game turn (table on #719).

A note on the perf-repeat label: the pr-fast micro families don't route through rt.Some, so the lane serves as a regression guard on the vm families here rather than the measurement of the win — the e2e numbers above are the claim.

New tests: prepared_call_test.go (repeat invocation, closure captures, the tail-call rebind reset, reuse after error, native/variadic and unsupported-arity rejection) and some_prepared_test.go (bytecode pred over chunked + linear seqs, native fallback). Suites green: pkg/vm, pkg/rt, test/; linux, js/wasm, plan9 builds.

@mparrett
mparrett force-pushed the perf/vm-prepared-call branch from ec1a462 to 4d5995f Compare August 12, 2026 16:28
An error occurred while trying to automatically change base from perf/vm-defer-free-dispatch to main August 12, 2026 19:03
mparrett and others added 3 commits August 12, 2026 12:40
…n some

A native seq fn calling a bytecode predicate pays resolution
(resolveBytecodeCall), frame-pool mutex traffic, and full frame init on
every element, although all of it is per-call-site constant. PreparedCall
resolves the callable once, owns one frame, and per call only resets
args/ip/sp plus the fields a callee can rebind: a tail call in the body
retargets the frame's code/consts/closedOvers via installBytecodeCall, and
an error unwind can leave stale handlers, so the reset covers those too
(pinned by tests). Non-bytecode and variadic targets return nil and callers
keep the generic ec.Invoke path.

Adopt it in rt.Some. On the #700 some-over-range workload (1e6 elements,
interleaved medians, darwin/arm64) this takes the callback-heavy shape from
~1.5x of v1.12.2 to below the v1.12.2 baseline while keeping #644's
stack-safety. reduce's closure path, every?, and the lazy-seq step fns are
the follow-up adopters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
native_prims.go is a manifest input; the some-adopter edit left the
committed digest stale, failing go test ./pkg/genmanifest on this head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Call1 is the only entry point, but PrepareCall accepted any arity:
arity 0 panicked at args[0], arity 2+ left Go nil interfaces in the
unpopulated slots and passed them into bytecode. Prepare only arity 1
so other arities fall back to the generic Invoke path; widen as CallN
methods land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mparrett
mparrett force-pushed the perf/vm-prepared-call branch from 4d5995f to ae28e9a Compare August 12, 2026 19:41
@mparrett
mparrett changed the base branch from perf/vm-defer-free-dispatch to main August 12, 2026 19:44
@mparrett
mparrett merged commit af4b7f4 into main Aug 12, 2026
5 checks passed
@mparrett
mparrett deleted the perf/vm-prepared-call branch August 12, 2026 20:56
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.

2 participants