Tail-call optimization in C is relatively recent (2025)
The Evolution of Tail-Call Optimization (TCO) in C

📜 Perspective 1: The Technical History of C's TCO
Posted by anton (subscriber, #25547) on Aug 21, 2025
Contrary to popular belief, tail-call optimization in the C language is ancient a relatively recent development. The primary obstacle was the traditional C calling convention, which dictates that the callee is not responsible for cleaning up the stack space allocated by the caller.
The Stack Dilemma
In older C standards, a function could be declared as int f();. This created a scenario where:
- The actual call might provide arguments.
- The function definition might expect parameters.
Mathematically, if , the stack would be left in an inconsistent state if the callee tried to clean it. Therefore, the caller had to perform the cleanup after the function returned. This requirement effectively turned every potential tail call into a standard call.
"When I looked in 1994 at the C compilers of the day, they did not perform tail-call optimization for the kind of usage shown in the article."
The Road to Modern Support
The timeline of GCC's evolution regarding TCO looks like this:
- 2001: Mark Probst introduced TCO to GCC, but it required a separate calling convention.
- Limitations: As noted in section 6.4 of his documentation, it could not handle indirect calls, which are essential for interpreter dispatch.
- Workarounds: For a long time,
goto *(computed gotos) provided a sufficient alternative.
Recently, the paper "Copy-and-Patch Compilation" by Xu and Kjolstad highlighted the power of TCO. Testing confirms that modern gcc and clang now support TCO for the patterns discussed in the Python performance context.
Impact on Gforth
The ability to utilize TCO opens new doors for Gforth. Currently, Gforth is limited to roughly 2,000 code snippets (for VM instructions and stack caching) because it relies on goto *.
| Feature | goto * System | TCO-based System |
|---|---|---|
| Snippet Capacity | ||
| Flexibility | Limited | High |
While the Python community has already leveraged this, Gforth has yet to implement it.
🎨 Perspective 2: Practical Application in Forth
Posted by lafp (subscriber, #89554) on Aug 23, 2025
I have recently developed a toy interpreter for a Forth variant that utilizes tail calls for instruction dispatch. This approach provided a significant architectural advantage: built-in functions now behave identically to instructions, eliminating the need for a specific "call built-in" opcode.
Project Goals & Status
The project is a variant of the Forth Haiku language, designed for creating art (similar to how ShaderToy works for GLSL).
- Implement TCO-based dispatch
- Support super-instructions
- Optimize for small-scale hardware
- Expose interpreter via web browser
- Control an LED matrix (inspired by Flaschen Taschen)
The source code is available here: https://github.com/lpereira/lwan/blob/master/src/samples/...
⚡ Perspective 3: High-Performance Wasm Interpreters
Posted by Robbepop (guest, #185603) on Aug 10, 2026
Recent benchmarks for the upcoming Wasmi (WebAssembly Interpreter) emphasize the critical nature of TCO. Wasmi utilizes direct-threaded code (via tail-calls) for dispatch, though it can be toggled to other modes.
Dispatch Method Comparison
The performance delta is staggering:
- Loop-Switch Dispatch: Slowest (Old-school approach).
- Indirect-Threading: Moderate.
- Direct-Threaded (Tail-Calls): Fastest.
Because of this, all top-tier Wasm interpreters—including Wasmi, Wasm3, and Stitch—rely on tail-calls. In my view, native TCO is not just a feature, but a fundamental requirement for any language claiming to be a true system programming language.
// Conceptual representation of a tail-call dispatch
void execute_instruction(Instruction *ins) {
switch (ins->opcode) {
case OP_ADD: return handle_add(ins->next); // Tail call
case OP_SUB: return handle_sub(ins->next); // Tail call
// ...
}
}
Copyright 2026, Eklektix, Inc. Comments and public postings are copyrighted by their creators.