Awhile back I wrote a post about tail-call optimizations that the F# compiler used to eliminate stack overflows. Brian McNamera commented about another optimization that I didn’t illustrate – the ‘tail’ opcode that appears when mutually-recursive and indirectly-recursive functions are encountered. Tail-call optimization is one of the really powerful features of F#, so I really wanted to see how this worked under the hood.
The first thing we need is a pair of mutually-recursive functions. The easiest (laziest? :)) way to do this is to write one function (e.g. f1) and duplicate its implementation as another name (e.g. f2):
// Hunting for tail calls
// 6.17.09
open
let
and
let
let
After compiling this, I popped open the resulting executable in Reflector. Of course I wasn’t going to find the ‘tail’ opcode by looking at the C# – I needed to disassemble the IL. I hunted and hunted for the ‘tail’ opcode, but couldn’t find it! Every call to f1 from f2 (and f2 from f1) used the stack to pass around n and acc. Even stranger – this is the first F# program I’d written using Visual Studio 2010, and I could have sworn that I’d done the same thing with F# in Visual Studio 2008 a couple months ago.
After building the project again, I noticed the command-line arguments passed to fsc:
“—tailcalls-“? Somehow tailcalls are being turned off. Maybe there’s something in the project settings that are disabling the tailcalls? Ah-ha! The checkbox was unchecked
After poking around a bit more, I discovered that by default the “Generate tail calls” box is unchecked fo Debug mode, and checked by default for Release mode. Hmmm, interesting.
After switching to Release mode, I rebuild the project and opened the .exe in Reflector. Here we go! There’s the elusive ‘tail’ opcode:
.method
}
(The IL code for sum2 looks identical.) Interestingly enough, the C# code from Reflector looks exactly the same between Debug and Release modes (with and without tail calls) – C# doesn’t have the capability to make tail calls.
Well, there you have it! We finally found the elusive ‘tail’ opcode.
That being said, be sure to keep this in mind – the default settings of Visual Studio 2010 for F# development are drastically different between Debug and Release mode. Bugs might crop up in Debug mode (e.g. StackOverflowExceptions) that don’t rear their heads in Release mode.
I think the motivation for this is that using tail calls severely limit the usefulness of the Visual Studio debugger since it relies on traversing the stack frame (that the tail opcode destroys) to display debugging information.
For example, without tailcall optimizations setting a breakpoint on sum1 looks like this:
The callstack shows some useful debugging information, specifically the values of n and the accumulator.
However, if we enable tailcall optimization, this breaks down after running through the breakpoint 10 times, each time it shows one line with different information:
… You get the idea.
Hope that sheds some light on tailcall optimization, as well as some of the new features of F# in Visual Studio 2010!