back
160 comments
> In 2001 Mark Probst implemented tail-call optimization in GCC

That's me.

The motivation back then was to allow compilers that target C to assume that tail calls will be "proper". That's different from an optimization, which is usually optional, and which compilers don't guarantee.

The LWN post briefly sketches why this is hard: C allows variable-argument functions (like printf) where only the caller knows for sure how many arguments it passed, which means that only the caller can clean up the stack, unless the stack frame size is also communicated, which "normal" C calling conventions don't do. But when the callee does a proper tail call, the stack frame that returns to the callee is not the stack frame that the callee originally sent. This is explained in more detail in my thesis starting on page 16: https://hostr.flingit.run/s/proper-tail-calls.pdf

That is a very cool contribution, I actually didn't know that it required a new calling convention! I look forward to reading your thesis
Let's assume that parameter are all the same size and put on a stack.

If you know that you are an M-parameter function being called, and you want to tail cal an N-parameter function, where N <= M, then you can just place the new N parameters in the same space on the stack where you received your M parameters, and jump to that function. That function will return to your original caller, which will remove the M parameters, not caring that some of them are not the originals that it passed.

Suppose N > M. Things start to get tricky. There isn't space in our original argument space for N. If we increase the space, the original caller won't clean it up properly. If we just allocate a new space of N, we are not making a tail call.

Because we want to make a tail call, it means we don't expect to execute any code in this function any more, and are free to trash the local variables. We can move the stack down a bit to make room for N arguments above where previously we were given M by our caller. To solve the problem that our caller wants to clean up M, but we need it to clean up N could be solved by a trampoline. We prime the stack such that when the tail-called function we are targeting returns, it will not go to our caller directly but to a stub function. That stub function will clean up the N-M words of the stack, leaving M, and then return to the original caller, which cleans up M.

In this situation, we are benefiting from knowing that the caller passed M to us. In the case of a variadic function, we don't know at all. It could just be the fixed arguments (parameters before the ellipsis) like printf("hello\n'), or any number. There is a run-time protocol to discover what parameters there are; the application logic figures it out from the arbitrary conventions. That's too late and too ad hoc for compile time.

I think yuo can reason about it similarly to above. If we are a variadic with M fixed parameters, we know we are called with at least M arguments, so we can place N <= M tail-callee arguments into the variadic space and proceed accordingly. For N > M, we can extend to make up the difference and use the trampoline to clean up and return to the original caller.

sThese trampolines are not closures; they are behind-the-scenes that can be generated as static code; no executable heaps or stacks required.

Unless the language can guarantee TCO, I don’t feel comfortable writing tail recursive code and being at the compiler’s/interpreter’s mercy.

I think the framing of TCO as an optimization has been very unfortunate.

It's hard to argue that it isn't an optimization, because it doesn't affect the semantics of the program. However most optimizations are very hard to observe. The vast majority of optimizations only affect code size and runtime. TCO is one of the few exceptions. It affects memory usage, and more sensitive stack memory at that. This is why a missed optimization can be so much more catastrophic and it is worth considering things like `musttail` attributes so that the code fails to compile rather than misses the optimization.

I can only think of a few other optimizations that affect memory usage. Register spilling (arguably not really an optimization but a necessity), Rust's niche filling for enum discriminants and C++'s std::vec<bool> (a language-level optimization, arguably a different thing entirely).

I often think about how few memory optimizations we have. The reason is most likely that they tend to be non-local so are much harder to apply than CPU optimizations that generally have no effect outside of the function they are in.

I think the problem with considering it a "pure optimization" is that code that is written to use tail-calls, if not optimized, is almost always unbounded recursive code. And modern OSes tend to have relatively small stack-size limits (relative to the kinds of huge data structures modern software slings around, incl. not only individually-"wide" structures, but also "deep" trees and graphs.)

Which means that "whether this naively-recursive code is actually recursive in practice" is a semantic difference, in that there is an error/failure-mode (stack overflow) that can be statically guaranteed to not happen (at least for a given compilation target) if TCO gets applied; but which cannot be guaranteed to not happen without TCO applied.

---

Tangent: you could of course try to write code defensively, to guarantee that a stack overflow won't occur, by bounding recursion separately (e.g. via a passed-and-decremented recursion-limit parameter), so that in the non-TCO case, you get a software exception thrown (which you'd hopefully then handle... somehow), rather than triggering a stack overflow.

And for many more-traditional recursive algorithms, this works!

But doing so for the types of algorithms that are "canonically" expressed in terms of tail-calls (even in a non-tail-call-idiomatic language like C), almost always requires poking holes in the C abstract machine to see through to the micro-architectural details underneath.

You can't just use something like a recursion-limit parameter as a general solution for these algorithms, as TCO is used in things like continuation-passing or threaded-code VM implementations — i.e. things that look less like visiting trees and more like visiting unboundedly-non-terminal infinite-state-machine states ["infinite" because the states are dynamic function pointers to JITted code, and more of them can appear at runtime.]

You need to not track the "number of invocations deep" you are into the algorithm, but rather, how big the stack actually is at the moment. Which means you need to actually do math on addresses of the stack base pointer vs either the stack pointer, or the address of a local stack-allocated variable. There's no version of that that doesn't require writing non-portable inline assembly.

> It's hard to argue that it isn't an optimization, because it doesn't affect the semantics of the program.

Depends on the semantics of the programming language itself. For some languages, it is truly an optimization, for some, it is required, and does meaningfully change observed semantics.

If the semantics of 'while (true)' was "will crash the program after an implementation-defined but often fairly low number of iterations", I would stop using 'while (true)'.
std::vector<bool> is just a terrible specialisation, it isn't an optimisation.

If std::vector<bool> was an optimisation we couldn't write C++ which blows up because it's actually a bitset, it would be semantically transparent - but that's easy to do even by accident because it's not transparent at all.

In fact the existing std::vector<bool> should just be named std::growable_bitset or something and then std::vector<bool> would make what you actually wanted like Rust's Vec<bool> does.

> because it doesn't affect the semantics of the program

It does when you use them as a feature and not an optimization. Like in interpreters, state machines, parsers, etc.

Calling tail calls an optimization set computer science back 40 years.

>It's hard to argue that it isn't an optimization, because it doesn't affect the semantics of the program

it is guaranteed in Scheme, and it affects the semantics of programs in a completely positive way.

Much of computer science is "pure" and "abstract" like mathematics. However, programmers are still taught to use loops to calculate factorial rather than recursion in order to avoid stack overflow. In Scheme you can use recursion without flinching. That is a semantic difference.

If my program crashes without it, that's a semantic difference no?
JVM does a lot of escape analysis to turn heap allocated memory into stack local variables.

It doesn't matter if it's local since it's a VM, it's doing it at runtime and can change an entire call stack of non local code for an optimization.

C# is an interesting case because it shares a common runtime with F#, and F# guarantees TCO in most circumstances ( try / catch can stop it ) .

There is a "tail" prefix in the intermediate language (IL) bytecode that F# uses but Roslyn, the C# compiler, never emits.

So unlike F#, whether the same algorithm written in C# becomes a loop depends on JIT behaviour. This means if you're coming to a function cold in C# you can overflow the stack, while if you enter the same function fresh after it's been warmed up, it may have been optimised away by RyuJIT and if so you are able to call it safely for what would be large numbers of recursions.

GCC has `[[gnu::musttail]] return`.

But yes, framing TCO as an optimization is unfortunate.

Some languages have TCO annotation, it throws compiler error if TCO fails. You want stronger type system, not smart compiler guarantees or promises!
You can rely on it now in gcc and clang, in the sense that they support a [[musttail]] attribute that tells the compiler to report an error if a call can't be TCO'd. The language doesn't guarantee TCO but can implement it at its option. If your program uses the attribute and still compiles, it means it has compiled with proper TCO.
Someone on here had the neat idea of a “become” keyword replacing “return” when TCO is desired. I thought it was the obvious route forward and remain confused why I still haven’t seen it adopted.
Indeed. I guess this is why [[gnu::musttail]] and [[clang::musttail]] exist.

https://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc/Statement-Attr...

I think Anton is replying to me in that LWN article IIRC. I personally didn't know C only had tail calls that late and learnt something new there!

On the other hand, I am pretty new to the compiler space myself, and I count early 2000s as a pretty long time ago, though again it is not that far back considering how long other language implementations had tail calls like in ML or variants since 1980-90s.

It still doesn't, this is a compiler specific language extension.

You won't find anything on ISO/IEC 9899:2024 about tail calls, like it happens on Scheme.

https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf

Section 3.5 of R7RS.

https://standards.scheme.org/official/r7rs.pdf

I think Anton is wrong. Since C89 and before C23 calling an `int f();` function with arguments not matching the definition's actuals is UB. In C23 `int f();` became the same as `int f(void);`, so calling that function with any arguments is a compile-time error.

For variadic functions, if you use `va_start()`/`va_arg()`/`va_end()` to consume all the arguments, and leave no `va_list` alive, then the compiler can correctly generate a tail call from such functions.

and TCO was added then removed from js! https://stackoverflow.com/a/54721813

This leads to fun stack-overflow bugs too in a lot of js code (one solution is to flatten: https://joshua.hu/javascript-infinite-tail-call-recursion-st...)

Lack of TCO is also a common footgun for Scheme programmers using Common Lisp.
Js really should have it. I think the shift in style from functional and manual prototype chains to Java classes is quite disappointing.
What practical patterns are enabled by TCO in C? My impression is that every tail call can written as a loop much more naturally. Tail calls are important in functional languages where you don't have mutable loop variables.

And imo they are an ugly hack even there - one of the few core constructs where its readily apparent you're not programming an abstract machine but a real, and limited computer. For example the most natural way to write factorial:

      let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1)
is not tail recursive, and will overflow if the compiler fails to optimize.
Not every tail call is for a loop.

You can have a set of mutually recursive functions, which tail call each other.

In C you can write state machines using "goto" (the implementations with "switch" are typically much more inefficient), but in languages with guaranteed tail call optimizations you can write a state machine where each state is a function.

In general, it is frequent enough to call another function as the last step of a function, even when there is no recursion involved. It is quite stupid for a compiler to use a CALL in such instances, instead of using a JMP. The only problem is that the function calling convention must be compatible with this optimization, while traditionally the C language used an inefficient calling convention that is not compatible with optimizations. That convention is a residue of the time when functions could be used without being declared and it should never be used by modern compilers.

> What practical patterns are enabled by TCO in C?

Continuation Passing Style - an important construction for interpreters, but which is also useful for compilers as it's a nice way to do control flow analysis, data flow analysis and more.

The missing feature is closures - functions which capture values from their static environment, which are basically needed to make CPS useful. GCC has nested functions, but they cannot capture without making the stack executable, which is terrible. There's a proposal[1] to get closures into C, but at present you need to simulate the capturing yourself, which is cumbersome, but can be done efficiently.

[1]:https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Functio...

> My impression is that every tail call can written as a loop much more naturally.

Which is more natural? (please just assume my wonky pseudo code syntax makes sense)

   printall(List) -> 
      foreach item in List {
         print_item(item)
     }.

   printall([Head | Tail]) ->
       print_item(Head),
       printall(Tail);
   printall([]) -> ok.

    
IMHO, both of these need to be taught, neither is particularly more natural. In addition, as others have described, TCO makes a lot of sense for interpreters and state machines.
> What practical patterns are enabled by TCO in C?

It's important in interpreters. Here's an example: https://blog.reverberate.org/2021/04/21/musttail-efficient-i...

It is simple to convert factorial to tail recursive form. In lua, which has tco:

    local factorial do
      local function impl(n, acc)
        if n == 1 then
          return acc
        else
          return impl(n - 1, acc * n)
        end
      end
      factorial = function(n)
        if n < 0 then
          error("factorial input is negative")
        elseif n <= 1 then
          return 1
        else
          return impl(n - 1, n)
        end
      end
    end
You could replace impl with an imperative loop:

    local acc = 1
    repeat
      acc = acc * n
      n = n - 1
    until n == 1
    return acc
Personally, I find this ugly compared to the tail recursive solution. The loop version only seems more natural if you primarily think in loops. Tail recursion is strictly more powerful than looping since every imperative loop can trivially be converted to a tail recursive function, but the reverse is not true.
TFA assumes pre-C89 C, I think:

> The caller could see the declaration int f();, the actual call could have n>0 arguments, and the actual function could have m≤n parameters.

Certainly if `f()` were `int f(void);` then that wouldn't be the case. But even for `int f();` C17 6.5.2.2p6 says that "If the number of arguments does not equal the number of parameters, the behavior is undefined." Near as I can tell that was made UB in C89. So TFA is a) right about K&R C, b) just wrong for pretty much all post-K&R C. C23 makes `int f();` be the same as `int f(void);`.

That calling a non-variadic function with more / fewer arguments than expected by its definition is UB is enough to make TCO possible for that function's body.

The point about K&R C is well taken though: to turn a tail call into a jump, the caller needs to know how much to pop off the stack.

For variadic if you `va_start()`, `va_arg()` as needed, then `va_end()` with no `va_copy()` left alive then you can still tail-call out correctly, otherwise you can't.

For non-variadic functions post K&R C TCO should always be possible and not UB, provided you're not triggering UB to begin with by using the incorrect number of arguments.

I recently played around with what I call "manual tail-call optimization": transform a tail call to a goto to the beginning of the function. Check it out: https://godbolt.org/z/3fY1v1oeW

  int factorial_loop_iterative(int n, int a){
    while(n > 0){
      a = a * n;
      n = n - 1;
    }
    return a;
  }
  
  int factorial_loop_recursive(int n, int a){
    if(n > 0){
      return factorial_loop_recursive(n - 1, a * n);
    }else{
      return a;
    }
  }
  
  int factorial_loop_manual(int n, int a){
  tailcall:
    if(n > 0){
      a = a * n;
      n = n - 1;
      goto tailcall;
    }else{
      return a;
    }
  }
  
  int (*factorial_loop)(int n, int a) = factorial_loop_manual;
  
  int factorial(int n){
    return factorial_loop(n, 0);
  }
I recommend against, of course! Incorrectly sequencing the manual version results in bugs (swap the assignment for n and a), which the recursive version doesn't need to care about.
Seems like a complex way to write a normal looped version. Apart from factorial_loop_manual() being one in design, its name even says as much.
GCC has had TCO since the 1980s I'm pretty sure. Since then it's been extended to work in more contexts.
> In 2001 Mark Probst implemented tail-call optimization in GCC with a separate calling convention; he lists the limitations of the then-existing tail-call optimization in GCC in section 6.4, among them: "It cannot handle indirect calls" (which would have been used in tail calls for interpreter dispatch).

Relatively recent being a quarter of century? Or at least a fifth of a century for indirect calls[1] (GCC 3.4.6 is the earliest I see on Compiler Explorer, released March 2006).

[1]: https://godbolt.org/z/vvcnn54oM

For people who passed their 30s, everything that happened after their 20th birthday is recent. For me, September 11 is recent memory, as well as the 2008 great recession.
Given that GCC was first released in 1987, that would mean that tail call optimization, including of indirect calls, has been around for more than half of GCC's lifetime. So it's indeed fair for the parent article to say that "[GCC has] had tail-call optimizations for most of [its] existence".
>That quote is the article, and it's a little surprising that it's buried so far into the content

Is it really surprising in 2026? Today's online writing style is not primarily designed to communicate. It's designed to keep the reader 'engaged' for as long as possible. The reader's time is a resource to be extracted.

I'm absolutely not poking this author individually. It's the writing style of the net.

I was wondering what this was in reference to; it's not in reference to TFA here. It's a quote from https://bytecode.news/posts/2026/08/because-it-s-not-fun-eno..., so presumably you meant to post this over at https://news.ycombinator.com/item?id=49242245.
> In 2001 Mark Probst implemented tail-call optimization in GCC

MSVC didn't add tail-call optimisation until sometime in the 2010s, IIRC.

I distinctly remember sending a tail-recursive C++ program to someone who developed on Windows, and it crashing, in the late mid-to-late 2000s.

MSVC stands for MicroSoft Visual C++ compiler AFAIK.

It famously doesn’t support a few features of C99.

They don’t really seem to care much about regular C support (non-C++).

what makes TCO so difficult to implement? it feels like it should be a very simple "if the final instruction before RET is CALL, then eliminate the call"

but clearly I'm missing something

In particular it's that if you return the result of a function call, then it's a tail call. For example, in `return f() + g();` neither the call to f() nor the call to g() are tail calls because they return into an expression (`+`) other than `return`.

If you scroll up you'll see a discussion of how the caller does the popping of arguments it pushed, so it has to be the case that if a different number of arguments were needed for a tail call then the caller will still pop the correct number of arguments, and that is where the complexity lies: because the caller does not actually know anything about the called function's tail call details, so how does one cause the correct thing to happen? One way is by changing the calling conventions radically to ensure that either the called function cleans up the arguments before returning, or that the number of bytes to pop is effectively part of the return signature of the function (with the caller somehow being careful to check that the advertised number wouldn't destroy its frame), or just arrange to leave exactly the number of bytes on the stack that the caller expects even if one tail-calls a function that would leave a different number of bytes.

(2025)