Tail Recursion Optimization

January 12, 2023 ยท 3 minute read

Recursion is popular, but it is also dangerous. If you don’t limit it, stack overflows are bound to happen, if you do not structure your recursive code in a way that the compiler can properly optimize it (if it supports the optimization).

Tail recursion is a technique coming from functional programming where the last operation in a recursive function call is the recursive call itself. Tail recursion optimization allows compilers to optimize tail recursive functions by eliminating the need to keep track of the call stack, thus reducing the amount of memory required to execute the function and preventing an overflow.

When a function is called recursively, the compiler creates a new stack frame on the call stack for each call. This stack frame contains information about the function call, such as the arguments passed, the return address, and the local variables. In a tail recursive function, the last operation before the recursive call is a return statement, so the compiler can safely discard the current stack frame and reuse it for the next recursive call.

Now the memory usage is constant, and the execution time is linear with the number of iteration, in contrast to a non-tail recursive function where each call creates a new stack frame which is kept until the function returns.

Different compilers handle tail recursion optimization differently. Some compilers, such as GCC, Clang, and the Microsoft Visual C++ compiler, have built-in support for tail recursion optimization. They are able to automatically detect tail recursive functions and optimize them accordingly. Other compilers, such as the Intel C++ compiler, require the programmer to explicitly enable tail recursion optimization using compiler flags.

In C++, C++11 introduced the [[noreturn]] attribute, which allows the programmer to inform the compiler that a function never returns, this can be used to easily influence or improve how the compiler applies optimizations. When a function marked with [[noreturn]] is called, the compiler knows that the function will not return and that the current stack frame can be discarded. This means that it can optimize away the stack unwinding process, which is the process of cleaning up the stack frames when a function exits. This can result in a significant performance improvement for tail recursive functions. Then there’s Tail Call Elimination. The compiler will try to eliminate the tail call entirely by replacing the tail call with a jump instruction, which does not push a new stack frame. This results in a similar effect to stack unwinding optimization, but it doesn’t require any stack frame to be created in the first place.