HN Debrief

Tail-call optimization in C is relatively recent (2025)

  • Programming
  • Compilers
  • Developer Tools

The article is about a subtle distinction that gets lost in casual talk about tail-call optimization. GCC had some tail-call support a long time ago, but C as a language was built around calling conventions that made proper tail calls awkward, especially for variadic functions, indirect calls, and old-style undeclared functions. Several commenters pushed on the article's historical framing. They argued that for non-variadic functions in post-C89 C, mismatched argument counts are already undefined behavior, so one big obstacle mostly belongs to K&R C and variadic cases, not modern C in general.

If you want recursion or interpreter dispatch to run in constant stack space in C or C++, treat tail calls as an explicit portability constraint, not a happy accident. Use `musttail` where available, check ABI and signature restrictions early, and assume cross-compiler behavior still diverges unless you test it.

Discussion mood

Interested but corrective. People broadly agreed the topic matters, especially for interpreters and stack-safe recursion, but many thought the article overstated how recent TCO is in GCC and blurred the line between language guarantees and compiler-specific support.

Key insights

  1. 01

    Proper tail calls were the real goal

    The original GCC implementation was not chasing a micro-optimization. It was trying to make C a viable backend target for languages that require proper tail calls. That framing matters because the hard part is not speed. It is preserving constant stack use under ordinary C calling conventions, especially when variadic calls mean only the caller knows how much stack to clean up.

    If you are compiling another language through C, do not assume "the C compiler optimizes tail calls" is enough. You need proper tail-call behavior across the calling patterns your generated code uses, or your backend will leak stack.

      Attribution:
    • mark-probst #1
  2. 02

    Modern C removed one old obstacle

    For non-variadic functions, the scary case of calling `int f();` with arbitrary argument counts is largely ancient history. Since C89, calling a non-variadic function with the wrong number of arguments is undefined behavior, and C23 tightens `int f();` further. That means one of the article's key roadblocks mostly survives in K&R C and true variadic functions, not in normal modern prototypes.

    Do not let folklore about pre-standard C drive your model of what modern compilers can do. If your code uses real prototypes and avoids variadics in hot tail-call paths, the remaining constraints are more about ABI details and compiler implementation than the core language.

      Attribution:
    • cryptonector #1 #2
  3. 03

    Space guarantees are what developers care about

    Calling TCO an optimization misses why people fight over it. A missed inlining pass hurts throughput. A missed tail call can turn a constant-space interpreter or CPS program into one that eventually crashes. That makes tail-call support operationally closer to a control-flow guarantee than to an ordinary speed tweak, even if formal language semantics classify it differently.

    When recursion depth is unbounded, review tail calls the same way you review memory ownership or locking. If constant stack space is required for correctness in production, enforce it with compiler checks or rewrite the control flow.

      Attribution:
    • kevincox #1
    • derefr #1
    • drdexebtjl #1
    • fsckboy #1
  4. 04

    musttail changes the engineering story

    `[[musttail]]` is the escape hatch people wanted for years. It lets code state that a call must be emitted as a tail call, and compilation fails if ABI rules, destructors, or signature mismatches get in the way. That turns TCO from a best-effort optimizer decision into a property you can test in CI.

    For code that depends on tail calls, make `musttail` part of the interface contract and add compiler coverage to your build matrix. You will catch fragile changes like added locals, changed signatures, or exception paths before they ship.

      Attribution:
    • throwaway81523 #1
    • LukeShu #1
    • vinkelhake #1
  5. 05

    Interpreter dispatch is the killer use case

    The strongest concrete use case was not factorial or list processing. It was interpreters, continuation-passing style, and function-per-state machines. In those designs, tail calls are a clean way to express jumps between dynamic states, and rewriting everything into loops or switches often makes the code worse or less modular.

    If you build VMs, parsers, or state-machine-heavy systems, tail calls are worth treating as a first-class design lever. Prototype the control flow early with your target compilers instead of assuming a clean recursive design will lower the way you want.

      Attribution:
    • sparkie #1
    • adrian_b #1
    • noelwelsh #1
  6. 06

    ABI and destructors still block many tail calls

    Even with `musttail`, the call has to fit ugly low-level rules. Some ABIs require compatible signatures beyond return type. C++ destructors and Rust `Drop` can mean the tail call is not actually the last operation. That is why languages like Scheme can promise proper tail calls more easily than C-family languages layered on existing ABIs.

    Expect tail-call portability to get worse the moment you add RAII, exceptions, or mixed-language boundaries. Keep tail-call-critical code in narrow, simple functions with compatible signatures and minimal cleanup work.

      Attribution:
    • fweimer #1
    • im3w1l #1
    • steveklabnik #1

Against the grain

  1. 01

    Loops are usually simpler in C

    For ordinary C code, tail calls often buy less than enthusiasts claim. If the control flow is really a loop, writing a loop is usually clearer to most C programmers, and classic examples like factorial need extra accumulator plumbing before they even become tail recursive. That weakens the case for treating TCO as a mainstream C coding style rather than a niche systems technique.

    Do not force recursive style into everyday C just because the compiler can sometimes optimize it away. Reserve tail-call-dependent designs for cases where they clearly improve structure, such as interpreters or mutually recursive state machines.

      Attribution:
    • torginus #1
  2. 02

    Standard C still gives no portability promise

    Compiler support has improved, but the language standard still does not give you Scheme-like guarantees. Even if a future C technical specification lands, a conforming implementation can recognize the syntax and still reject programs that use it. That means the practical story remains toolchain-specific for years.

    If you need guaranteed tail calls across vendors, assume you do not have them in portable C today. Pin your compiler set or choose a language and runtime that specify the behavior you need.

      Attribution:
    • pjmlp #1 #2
    • Someone #1

In plain english

`musttail`
A compiler attribute that requires a call to be emitted as a tail call or else causes a compile error.
ABI
Application Binary Interface, the low-level calling, layout, and binary compatibility rules that compiled code must follow.
C23
The 2023 revision of the C language standard.
C89
The 1989 ANSI standard for the C programming language, the first formal standard for C.
CPS
Chicago Public Schools, the public school system serving Chicago.
MSVC
Microsoft Visual C++, Microsoft's C and C++ compiler toolchain.
proper tail calls
A stronger guarantee that tail calls always run in constant stack space, not just when a compiler happens to optimize them.
TCO
Tail-call optimization, a compiler technique that reuses the current function's stack frame for a call made as the function's final action.
variadic function
A function that can take a variable number of arguments, such as `printf`.

Reference links

Standards and proposals

Deep technical references

Language ecosystem examples

Compiler and platform history