Skip to content

perf: bind every direct call through one path - #86

Merged
owenthcarey merged 15 commits into
mainfrom
perf/cheap-calls
Oct 23, 2026
Merged

owenthcarey merged 15 commits into
mainfrom
perf/cheap-calls

Conversation

@owenthcarey

@owenthcarey owenthcarey commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Item 1 of the plan in docs/PERFORMANCE-PROGRESS.md (added here): the call paths.

A direct call between two compiled functions bound its activation through several paths (core_bind_plain::<true> for positional calls, core_bind_kw_direct for keyword calls, bind_call_cells for callees with cells), with per-site warm slots. They now share one bind, frame_jit::direct_enter, into an ordinary InlineAct; the variants are gone.

The branch first tried light direct activations, which skipped the core loop's switch and the pending list (go retired 6% fewer instructions). They were dropped in the second commit: anything that switches activations had to reify them first, which kept constructors, generator resumes and builtin lanes off them, took a frame swap and 15 reify points, and borrowed code that a reassigned __code__ could free mid-call.

Instructions on the call-heavy workloads are within 0.3% of main (geomean 0.997 over go, raytrace, deepcopy, tomllib_loads, mini_interpreter, sudoku, chaos, exceptions): this PR is a simplification, not a speedup. The progress note records why the remaining call cost (about 480 instructions a direct call) is spread over the activation protocol rather than any one step.

Validation

  • Bundled regression suite: 423/423 (with the new test_light_direct_calls.py, which covers frames, tracebacks, locals, finalizer timing, the recursion limit and warning stack levels from hot direct calls).
  • VM unit tests (RUST_MIN_STACK=8388608, all features): 430/430.
  • Curated CPython 3.14 gate (light-call version): the same 7 unexpected results as the branch merged in perf: close the gap to CPython on realistic workloads #85.

Record where WeavePy stands against CPython 3.14 (warm time 1.28x,
peak memory 1.71x, startup 0.74x), the callgrind breakdown of the
remaining gap, and the ranked plan for the structural changes.
A call between two compiled functions bound a pooled InlineAct,
pushed it on the core loop's switch, and entered its caller on the
lazy spine's pending list, about 480 instructions in the call helper
alone. It now binds a per-depth LightAct record instead: the callee's
frame and native state, with its code, globals and builtins borrowed
from the callable and no switch or pending-list work at all.

Nothing outside the call sees a light activation. Whatever needs the
core loop's view of it first reifies it, and the light activations
that called it, into the ordinary direct activations the old path
bound: the callee leaving its native code, any helper that switches
the running activation or publishes its caller (ensure_heavy), and a
return that isn't clean. A site whose light calls keep being reified
binds ordinary activations instead.

Positional and keyword calls, operator dunders, property getters and
callees with cells all take the light path; core_bind_kw_direct is
gone. go retires 6% fewer instructions, tomllib_loads 3%, deepcopy
1%; the suite's geomean 0.7%.

New burst-stat lines count light calls and reifications, and a
regression test covers frames, tracebacks, locals, finalizer timing,
the recursion limit and warning stack levels from hot direct calls.
Replace the light activations with one bind for every direct call
between compiled functions (frame_jit::direct_enter): positional and
keyword arguments, defaults, and fresh or closure cells, into a pooled
or site-warm InlineAct pushed on the core loop's switch, as the core
loop binds its own calls.

The light records skipped the switch and pending-list registration,
about 40 instructions a call, but anything that switched activations
had to reify them first, which kept constructors, generator resumes
and builtin lanes off them and took 15 call sites and a frame swap to
get right. They also borrowed the callee's code, which a reassigned
__code__ could free mid-call. With one representation, every shape
runs direct and nothing needs reifying: core_bind_kw_direct,
bind_call_cells and core_bind_plain's DIRECT variant are gone.

Instructions are within 0.3% of before on the call-heavy workloads
(the geomean of go, raytrace, deepcopy, tomllib_loads,
mini_interpreter, sudoku, chaos and exceptions is 0.997). The next
step emits the call in line from compiled code against this record.
@owenthcarey owenthcarey changed the title perf: one cheap activation protocol for Python calls perf: bind every direct call through one path Oct 21, 2026
Record the design the call-path measurements point to: activations
as records on a per-thread stack, linked to their callers, that the
interpreter, the frame JIT and generators all run, replacing the
pooled InlineAct, the lazy pending list, the per-call native State and
the frameless leaf paths. The RFC gives the measured per-call cost
breakdown, a five-stage plan, and the expected effect.
@owenthcarey
owenthcarey marked this pull request as ready for review October 23, 2026 19:55
@owenthcarey
owenthcarey merged commit 1291398 into main Oct 23, 2026
28 checks passed
@owenthcarey
owenthcarey deleted the perf/cheap-calls branch October 23, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant