C language
In the C programming language, it's easy to have tail recursion:
int foo(...) {
return foo(...);
}
Just return as is the return value of the recursive call. It is especially important when this recursion may repeat a thousand or even a million times. It would use a lot of memory on the stack.
Rust
Now, I have a Rust function that might recursively call itself a million times:
fn read_all(input: &mut dyn std::io::Read) -> std::io::Result<()> {
match input.read(&mut [0u8]) {
Ok ( 0) => Ok(()),
Ok ( _) => read_all(input),
Err(err) => Err(err),
}
}
(this is a minimal example, the real one is more complex, but it captures the main idea)
Here, the return value of the recursive call is returned as is, but:
Does it guarantee that the Rust compiler will apply a tail recursion?
For instance, if we declare some variable that needs to be destroyed like a std::Vec
, will it be destroyed just before the recursive call (which allows for tail recursion) or after the recursive call returns (which forbids the tail recursion)?
Shepmaster's answer explains that tail call optimization, which I prefer to call tail call elimination, is not guaranteed to happen in Rust. But that's not the whole story! There are a lot of possibilities between "never happens" and "guaranteed". Let's take a look at what the compiler does with some real code.
Does it happen in this function?
As of right now, the latest release of Rust available on Compiler Explorer is 1.39, and it does not eliminate the tail call in
read_all
.Notice this line:
call qword ptr [rip + example::read_all@GOTPCREL]
. That's the recursive call. As you can tell from its existence, it was not eliminated.Compare this to an equivalent function with an explicit
loop
:which has no tail call to eliminate, and therefore compiles to a function with only one
call
in it (to the computed address ofinput.read
).Oh well. Maybe Rust isn't as good as C. Or is it?
Does it happen in C?
Here's a tail-recursive function in C that performs a very similar task:
This should be super easy for the compiler to eliminate. The recursive call is right at the bottom of the function and C doesn't have to worry about running destructors. But nevertheless, there's that recursive tail call, annoyingly not eliminated:
It turns out that tail call optimization is not guaranteed in C, either. I tried Clang and gcc under different optimization levels, but nothing I tried would turn this fairly simple recursive function into a loop.
Does it ever happen?
Okay, so it's not guaranteed. Can the compiler do it at all? Yes! Here's a function that computes Fibonacci numbers via a tail-recursive inner function:
Not only is the tail call eliminated, the whole
fibonacci_lr
function is inlined intofibonacci
, yielding only 12 instructions (and not acall
in sight):If you compare this to an equivalent
while
loop, the compiler generates almost the same assembly.What's the point?
You probably shouldn't be relying on optimizations to eliminate tail calls, either in Rust or in C. It's nice when it happens, but if you need to be sure that a function compiles into a tight loop, the surest way, at least for now, is to use a loop.
Tail calls are guaranteed whenever your recursive function is called in tail position (basically the last statement of the function).
Tail call optimization is never guaranteed by Rust, although the optimizer may choose to perform it.
It's my understanding that this is one of the sticking points, as changing the location of destroyed stack variables would be contentious.
See also: