Repository navigation
Conversation
…waits `await f()` on an `async def` no longer allocates a coroutine object unless the body actually suspends. GET_AWAITABLE now also pushes the NULL and None that start the SEND loop, so the compiler emits a single instruction between an awaited expression and SEND. RETURN_GENERATOR uses that opcode in the caller to recognize an immediately awaited call: the body then keeps running in the already-pushed thread frame (marked lazy_coro, with its own exception-state item on the data stack) and the caller's frame is set up as SEND_GEN would leave it. A body that returns or raises without suspending never creates a coroutine object; YIELD_VALUE materializes one when the body first suspends, sized from the frame's code object. The eager path is kept for callers from C, instrumented SEND/END_SEND, coroutine origin tracking, JIT trace recording and tier 2. frame.f_generator and PyFrame_GetGenerator() return None/NULL for such a coroutine until it first suspends; asyncio.graph classifies frames by co_flags instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
e1ad90f to
e3e43cb
Compare
Documentation build overview
6 files changed ·
|
|
I considered converting Also, the initial source kept the byte code unchanged (
|
|
If we support the idea, I could prepare a PR for |
What
await f()on anasync defno longer allocates a coroutine object unless the body actually suspends.GET_AWAITABLEnow also pushes theNULLandNonethat start theSENDloop, so the compiler emits one instruction between an awaited expression andSEND. This is the static markerRETURN_GENERATORkeys on, in the spirit of the super-instruction idea from Enable informing callee it's awaited via vector call flag #91121.RETURN_GENERATOR(tier 1 only): if the caller's next instruction isGET_AWAITABLE, the body keeps running in the already-pushed thread frame, markedlazy_coro. The caller's frame is set up exactly asSEND_GENwould leave it, soRETURN_VALUEor an exception lands in the right place with no object created._YIELD_VALUE: a thread-owned frame reaching a yield is a lazy coroutine, the coroutine is materialized. Nestedawaitchains materialize innermost-first, once each.coro.send,functools.partial, ...), an instrumentedSENDorEND_SEND, coroutine origin tracking, while the JIT is recording a trace, and in tier 2 (_RETURN_GENERATORstays a frame pop).Why
A coroutine body that returns before its first
await(fast paths, cache hits, bookkeeping-only__aenter__/__aexit__) pays for allocation,_PyFrame_Copy, GC tracking and deallocation for nothing.Release build (no PGO/LTO), x86-64, per
await:await noop()(rawsend)await noop()(asyncio.run)async withno-op CMawaitthat suspends once (rawsend)Reviewer notes
frame.f_generator/PyFrame_GetGenerator()returnNone/NULLfor a lazy coroutine until it first suspends.asyncio.graphnow classifies frames byco_flags; documented indatamodel.rst,inspect.rst,frame.rstand the whatsnew porting section.sys.exc_info()across a suspension inside a handler, tracebacks,__import__locals,sys.settraceandsys.monitoringevent sequences (includingSTOP_ITERATION) are unchanged versus eager; covered byLazyCoroutineTestintest_coroutines.py.SENDis rewritten (backward-jump target), so the optimization is off everywhere there; events match eager.test_opcache.test_send_with: its__aenter__now suspends once, since theSENDit checks is otherwise never executed.🤖 Generated with Claude Code