A recursive function can leave a tower of callers waiting for results. If each caller has nothing left to do, could the next call take its place? Step through the sum, then compare Tail, TCO, and Loop at the same starting value.
- Live sum frames
- 1
- Peak sum frames
- 5
- Returned value
- pending
unsigned Sum(unsigned n) { if (n == 0) return 0; return n + Sum(n - 1);}Highlighted line is active
Function activations · newest first
- Sum(4)Active frame
sum(4) must wait to add 4 after its child returns.
Teaching model, not a compiler run. Tail keeps ordinary calls; TCO assumes successful tail-call elimination. Other compiler rewrites, caller frames and runtime frames are omitted.
What makes a call a tail call?
A call is in tail position when its result becomes the caller’s result directly, with no work left in that caller after the call finishes. Tail recursion is the special case where the callee is the same function.
A continuation describes what happens next with a result. Continuation-passing style makes that pending work explicit as a function, so the meaning of “nothing left to do” becomes visible.
The ordinary sum has a pending addition:
unsigned Sum(unsigned n) {
if (n == 0) return 0;
return n + Sum(n - 1); // Add n after the child returns.
}
An accumulator moves that addition before the call:
unsigned Sum(unsigned n, unsigned acc = 0) {
if (n == 0) return acc;
return Sum(n - 1, acc + n); // Forward the result unchanged.
}
Writing a tail call makes the transformation possible. Tail-call optimization, or tail-call elimination, is the compiler or runtime transformation that removes the need to keep the caller’s activation around. Select Tail in the figure to see what happens when the source is tail-recursive but this transformation is absent.
In the illustrated execution, ordinary recursion and unoptimized tail recursion reach n + 1 sum frames. Successful elimination keeps the sum’s stack usage constant. The number of additions remains proportional to n; the optimization changes the return path and stack growth, rather than the amount of arithmetic.
These examples use small nonnegative integers. Other compiler transformations are deliberately excluded from the figure: a compiler could also recognize the ordinary sum and convert it into a loop or simplify it further. The source spelling alone does not determine the generated machine code.
What changes in the machine code?
An ordinary machine call records where execution should return. A tail call prepares the callee’s arguments, releases the caller’s activation as needed, and transfers control so the callee eventually returns directly to the earlier caller.
For a call to a different function, the central difference can look like this in x86-64 assembly. This is a schematic comparison; register moves and stack teardown depend on the ABI and compiler:
; Ordinary call: come back here, then return again.
call Next
ret
; Tail call: Next returns directly to our caller.
jmp Next
For self-recursion, the compiler can instead update the arguments and jump to a loop header inside the same function. That produces the same control-flow shape as the figure’s explicit loop:
unsigned Sum(unsigned n) {
unsigned acc = 0;
while (n > 0) {
acc += n;
n -= 1;
}
return acc;
}
Tail calls can also connect different functions. Interpreter dispatch can transfer from one opcode handler to another using this same mechanism. That is the connection to Python’s newer interpreter.
Python 3.14: tail calls inside the interpreter
CPython 3.14 introduced an optional interpreter that implements opcode handlers as small C functions and tail-calls between them. Python functions keep their existing behavior, including recursive calls and their frames. Python 3.14 release notes
Follow add(7, 5) through a conceptual opcode sequence. Switch dispatch modes at any step: the Python value stack and result stay the same while the C control flow changes.
- Python source
return a + b- Live add() frames
- 1
- Result
- pending
Separate C handler functions
- LOAD_FAST a
- LOAD_FAST b
- BINARY_OP +
- RETURN_VALUE
// Inside one C opcode handlerexecute_current_opcode();opcode = read_next_opcode();[[clang::musttail]]return handlers[opcode](state);Highlighted line is active
Python value stack · top at right
Enter add(a, b). One Python frame holds its locals and value stack.
Conceptual bytecode and C pseudocode; caches, specialization and error paths are omitted. Both dispatch forms avoid adding a native stack frame per opcode. Python function calls still create Python frames.
The existing dispatch path
The interpreter executes Python bytecode. Each opcode reads or changes interpreter state, then dispatches to the next opcode.
CPython already supports computed-goto dispatch on suitable compilers: opcode handlers are labels inside one large C evaluator, and an indirect jump selects the next label. A switch-based path also exists. The tail-call interpreter introduces another way to connect those handlers; it does not replace a Python-level function call for every instruction. CPython 3.14 dispatch definitions
The new dispatch path
In the tail-call build, handlers are separate C functions with a common signature. Each passes interpreter state, including the Python frame, value-stack pointer and instruction pointer, onward to the next handler. Dispatch ends in a guaranteed tail call. CPython’s handler and dispatch macros
The important distinction is between two kinds of frame:
| Frame | What it represents | Effect of tail-call dispatch |
|---|---|---|
| Native C activation | Executing an interpreter handler | Transfers to the next handler without building an ever-growing native call chain |
| Python interpreter frame | A Python function call, its locals and evaluation state | Preserved according to normal Python call semantics |
The Python value stack shown in the figure holds operands inside one Python frame. Its two entries during addition are two values, not two function calls. Both computed goto and tail-call dispatch avoid a native frame per opcode; the benefit is how the compiler organizes and optimizes the handlers.
Why musttail and preserve_none matter
Separating handlers would be unsafe for an indefinitely running interpreter if each dispatch accumulated another ordinary C call. CPython uses [[clang::musttail]] to require a tail call at the dispatch boundary. Clang diagnoses an unsupported use instead of silently accepting an ordinary call. Clang’s musttail contract
The handlers also use the preserve_none calling convention. It makes more registers available for arguments and minimizes callee-saved general-register obligations, helping pass state between handlers efficiently. This is a compiler-specific internal ABI, not an interface for Python extensions. Clang’s preserve_none documentation
What is required to enable it?
For CPython 3.14, this is a build-time option, --with-tail-call-interp. The build needs proper tail-call support and the preserve_none convention; the release documentation lists Clang 19 or newer on x86-64 and AArch64. Profile-guided optimization is strongly recommended. Installing Python 3.14 alone does not establish that this mode is enabled. Configure options, 3.14 platform requirements
For a suitable CPython source tree and toolchain, the relevant configuration is:
CC=clang-19 ./configure --with-tail-call-interp --enable-optimizations
This selects interpreter implementation and PGO during a source build. It is not a flag passed to a Python script. A packaged interpreter’s build configuration depends on its distributor.
What do the performance numbers mean?
The 3.14 release notes report preliminary 3–5% geometric-mean improvement on pyperformance, varying with platform and architecture, against a Python 3.14 build using Clang 19 without the tail-call interpreter. These are upstream benchmark results, not measurements from this page. Documented benchmark context
Earlier, larger claims were corrected after an LLVM 19 compiler bug inflated the apparent improvement. The original CPython issue records that correction. Treat build settings, compiler version and workload as part of any comparison. Upstream correction
The figure intentionally has no speed counter: it illustrates control flow, not timing. To measure your workload, compare builds of the same CPython revision with matched toolchains and PGO settings, changing the dispatch mode, then repeat representative benchmarks.
Does Python now optimize tail recursion?
CPython still creates Python frames for recursive Python function calls. Its 3.14 tail-call interpreter changes internal C dispatch; it does not provide Python-function tail-call elimination or remove the recursion limit. Python’s explicit distinction
This function is tail-recursive, but deep calls still encounter the interpreter’s recursion limit:
# examples/tail_sum.py
def tail_sum(n, acc=0):
if n == 0:
return acc
return tail_sum(n - 1, acc + n)
Use iteration when constant call-stack usage is a requirement in Python:
# examples/iterative_sum.py
def iterative_sum(n):
acc = 0
while n > 0:
acc += n
n -= 1
return acc
Both examples assume a nonnegative integer. Python integers can grow as the sum grows, so constant call-stack usage does not imply constant total memory for arbitrary-size inputs.
For mutually recursive control flow, a trampoline offers an explicit loop that runs deferred calls. Its walkthrough follows how each worker returns to the loop before the next worker starts.
When can a compiler eliminate a tail call?
In C++, inspect the generated code for the actual target and build configuration. A return statement is not by itself a portable guarantee of elimination. The compiler must preserve program behavior, argument passing and object lifetimes.
A common obstacle is cleanup after a call:
Result Forward(Request request) {
Guard guard;
return Next(request); // guard's destructor must still execute.
}
If Guard has a nontrivial destructor whose effects must occur after Next, the caller still has work to do. RAII and object lifetime explain that return path. Clang’s explicit musttail extension imposes restrictions on signatures, calling conventions and destructibility; ordinary optimization remains target- and compiler-dependent. Clang restrictions
For the practical verification loop, start with Compiler Optimization: select the real flags, inspect assembly or diagnostics, and measure. Tail position describes the source-level opportunity; generated code establishes whether the transformation happened.
Related concepts
C++ compiler optimization lab notebook — compare optimization levels, inspect compiler rewrites, diagnose auto-vectorization, and build production verification commands.
Trace a small virtual machine through branches and loops, then compare switch, computed goto, and tail-call dispatch without changing its bytecode semantics.
Deep dive into Python's asyncio library, understanding event loops, coroutines, tasks, and async/await patterns with interactive visualizations.
Follow direct-style and CPS factorial through the same calculation. See how closures carry pending work, why Python still grows call depth, and where trampolines fit.
Follow a C++ function from tokens to a simplified AST, then inspect exact source ranges, declaration references and types in a keyboard-accessible explorer.
Understand the complete C++ compilation pipeline from source code to object files. Learn preprocessing, parsing, code generation, and optimization stages.
