GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Add ASYNC_CALL_* superinstructions for awaited calls to coroutine functions by asvetlov · Pull Request #161547 · python/cpython

Conversation

@asvetlov

@asvetlov asvetlov commented Nov 7, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

await f(...) where f is a coroutine function currently executes CALL (which runs RETURN_GENERATOR in the callee and returns to the caller), GET_AWAITABLE, PUSH_NULL, a load of None and SEND before the coroutine body starts.

This PR adds four specializations to the CALL family, ASYNC_CALL_PY_EXACT_ARGS, ASYNC_CALL_BOUND_METHOD_EXACT_ARGS, ASYNC_CALL_PY_GENERAL and ASYNC_CALL_BOUND_METHOD_GENERAL, and two to the CALL_KW family, ASYNC_CALL_KW_PY and ASYNC_CALL_KW_BOUND_METHOD. The specializer picks them when the callee is a coroutine function and the call is awaited at once (await f(...), async with, async comprehensions). They are superinstructions: they merge the call with the first execution of the four instructions that follow it, creating the coroutine and pushing its frame directly, with the caller left as if it were executing the SEND.

Design notes:

  • The specializations reuse the uops of the matching CALL_* or CALL_KW_* specialization; the only new uop is _ASYNC_CALL_GEN_FRAME, plus a tier-1 guard that the skipped instructions are not instrumented. The specializer reuses specialize_py_call() and specialize_py_call_kw(). The frame set-up shared with RETURN_GENERATOR, SEND_GEN and FOR_ITER_GEN is factored into gen_take_frame() and gen_link_frame(), and _Py_MakeCoroWithFrame() is _Py_MakeCoro() with the frame passed explicitly.
  • Every other CALL and CALL_KW path is untouched, so calls that cannot be merged (non-coroutine callables, types.coroutine functions, CALL_FUNCTION_EX) keep their current specializations.
  • RETURN_GENERATOR only pushes the None that the following POP_TOP discards when its frame already belongs to a generator, so coroutine functions whose prologue contains COPY_FREE_VARS or MAKE_CELL use the fast path too. The JIT trace recorder records such a RETURN_GENERATOR as a load of None, so the trace continues into the coroutine body.
  • The tier-2 interpreter traces through the new uop (_ASYNC_CALL_GEN_FRAME + _PUSH_FRAME).

Micro-benchmarks (release build, no PGO, ns per awaited call including loop overhead): await func(x) 50.5 → 44.2, await obj.method(x) 52.2 → 47.0, await closure(x) 60.0 → 55.0, a 4-deep chain that suspends 278 → 251. Keyword calls: await func(x, b=1) 69.3 → 63.0, await obj.method(x, b=1) 71.5 → 65.6. Awaits of plain functions or classes are unchanged.

Tested with the full test suite on a debug build with --enable-experimental-jit=interpreter, and the async-related tests on a free-threaded debug build. Also tested with the async-related and JIT tests on a release build with the LLVM JIT (--enable-experimental-jit), where await f(i) in a loop goes from 54 to 47 ns per iteration, an awaited closure from 64 to 55 ns, and a 4-deep suspending chain from 341 to 290 ns. Not tested: Windows, the tail-calling interpreter.

Also in this PR: a trace function can no longer jump to the def line of a generator frame, which re-executed RETURN_GENERATOR in a frame that already belonged to the generator (undefined behaviour before this change).

Still to do: file an issue and add the NEWS entry.

This PR was developed with the help of an AI tool, which drafted the implementation and tests; the design, measurements and review are mine.

🤖 Generated with Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
asvetlov and others added 16 commits November 7, 2026 10:49
@asvetlov @claude
Add ASYNC_CALL_* superinstructions for awaited calls to coroutine fun…
592e160
…ctions

Specialize CALL to ASYNC_CALL_PY_EXACT_ARGS, ASYNC_CALL_BOUND_METHOD_EXACT_ARGS,
ASYNC_CALL_PY_GENERAL or ASYNC_CALL_BOUND_METHOD_GENERAL when the called
function is a coroutine function and the call is awaited at once, as in
`await f(...)`, `async with` and async comprehensions.

These superinstructions merge the CALL with the first execution of the
GET_AWAITABLE, PUSH_NULL, LOAD_COMMON_CONSTANT None and SEND instructions
that follow it: the new coroutine is started directly instead of being
returned to the caller first. They reuse the uops of the matching CALL_*
specialization, with the new _ASYNC_CALL_GEN_FRAME uop in place of
_SAVE_RETURN_OFFSET, and the specializer reuses specialize_py_call().

RETURN_GENERATOR now does nothing in a frame that already belongs to a
generator, so that coroutine functions whose prologue has COPY_FREE_VARS
or MAKE_CELL can use the fast path too.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Drop redundant comments in ASYNC_CALL_* macros
9ed5b31
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov
Merge remote-tracking branch 'upstream/main' into claude/async-call-s…
f89eac1
…uperinstruction-67cf73
@asvetlov @claude
Remove comments that restate the code
0e7d877
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Trace RETURN_GENERATOR of a coroutine started by ASYNC_CALL_* as a No…
885cc1a
…ne load

Ending the trace there cut the trace of every awaited call to a closure
or to a coroutine function with cells, since COPY_FREE_VARS and
MAKE_CELL precede RETURN_GENERATOR in such frames.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Keep the Emscripten/WASI skip on CoroAsyncIOCompatTest
e62a1ab
The new AwaitCallTest class was inserted between the decorator and
the class it applied to.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Drop an inlined comprehension from test_async_with
5e36f9b
In a function scope the async comprehension is inlined, so no call
to a comprehension function is awaited there.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Reject f_lineno jumps into the prologue of a generator frame
88b811b
A trace function could jump to the "def" line of a generator, which
re-executed RETURN_GENERATOR in a frame that already belongs to one.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Assert SEND_GEN on a stored coroutine; fix a comment
b007c07
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Share the generator frame set-up between RETURN_GENERATOR and the gen…
1249117
… frame uops

_Py_MakeCoro() takes the frame of the generator function explicitly,
gen_take_frame() moves that frame into the generator, and
gen_link_frame() links a generator frame to the frame entering it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Add ASYNC_CALL_KW_PY and ASYNC_CALL_KW_BOUND_METHOD
8d25e78
Awaited calls with keyword arguments compile to CALL_KW, which gets
the same two awaited specializations as CALL for Python functions
and bound methods.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Keep _Py_MakeCoro() as is and add _Py_MakeCoroWithFrame()
462db8d
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Declare _Py_MakeCoroWithFrame() with PyAPI_FUNC like _Py_MakeCoro()
d4107d8
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Drop the comment on _Py_MakeCoroWithFrame()
539e937
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Shorten the comment in RETURN_GENERATOR
6b30556
Co-Authored-By: Claude Fable 5.1 <[email protected]>
@asvetlov @claude
Match the awaited call tail in one place
5b3c7b4
_PyCode_AwaitedCallSend() returns the SEND of `await f(...)` and is
used by the specializer, the tier-1 guard and _ASYNC_CALL_GEN_FRAME.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@markshannon markshannon Awaiting requested review from markshannon markshannon will be requested when the pull request is marked ready for review markshannon is a code owner
@tomasr8 tomasr8 Awaiting requested review from tomasr8 tomasr8 will be requested when the pull request is marked ready for review tomasr8 is a code owner
@Fidget-Spinner Fidget-Spinner Awaiting requested review from Fidget-Spinner Fidget-Spinner will be requested when the pull request is marked ready for review Fidget-Spinner is a code owner
@AA-Turner AA-Turner Awaiting requested review from AA-Turner AA-Turner will be requested when the pull request is marked ready for review AA-Turner is a code owner

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@asvetlov
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.