27

Possible Duplicate:
Why doesn't .net/C# eliminate tail recursion?

Take the following C# code:

using System;

namespace TailTest
{
    class MainClass
    {
        public static void Main (string[] args)
        {
            Counter(0);
        }

        static void Counter(int i)
        {
            Console.WriteLine(i);
            if (i < int.MaxValue) Counter(++i);
        }
    }
}

The C# compiler (mine anyway) will compile the Counter method into the following CIL:

.method private static hidebysig default void Counter (int32 i) cil managed 
{
.maxstack 8
IL_0000:  ldarg.0 
IL_0001:  call void class [mscorlib]System.Console::WriteLine(int32)
IL_0006:  ldarg.0 
IL_0007:  ldc.i4 2147483647
IL_000c:  bge IL_0019
IL_0011:  ldarg.0 
IL_0012:  ldc.i4.1 
IL_0013:  add 
IL_0014:  call void class TailTest.MainClass::Counter(int32)
IL_0019:  ret 
}

The problem with the above code is that it will cause a stack overflow (at about i=262000 on my hardware). To get around this problem, some languages do what is called tail-call elimination or tail-call optimization (TCO). Essentially, they turn the recursive call into a loop.

My understanding is the the 64-bit implementation of the .NET 4 JIT now performs TCO and avoids the overflow on code like the above CIL. However, the 32-bit JIT does not. Mono does not seem to either. It is interesting that the JIT (which is under time and resource pressure) does TCO while the C# compiler does not. My question is why isn't the C# compiler itself more TCO aware?

There is a CIL instruction that tells the JIT to perform TCO. For example, the C# compiler could instead generate the following CIL:

.method private static hidebysig default void Counter (int32 i) cil managed 
{
.maxstack 8
IL_0000:  ldarg.0 
IL_0001:  call void class [mscorlib]System.Console::WriteLine(int32)
IL_0006:  ldarg.0 
IL_0007:  ldc.i4 2147483647
IL_000c:  bge IL_001c
IL_0011:  ldarg.0 
IL_0012:  ldc.i4.1 
IL_0013:  add 
IL_0014:  tail.
IL_0017:  call void class TailTest.MainClass::Counter(int32)
IL_001c:  ret 
}

Unlike the original, this code will not overflow and will run to completion even on the 32-bit JIT (both .NET and Mono). The magic is in the tail. CIL instruction. Compilers like F# will generate CIL that includes this instruction automatically.

So my question is, is there a technical reason that the C# compiler does not do this?

I get that it has historically maybe just not been worth it. Code like Counter() has not been common in idiomatic C# and/or the .NET framework. You could easily view TCO for C# as an unnecessary or premature optimization.

With the introduction of LINQ and other things, it seems like both C# and C# developers are moving in more functional directions. So, it would be nice if using recursion was not such an unsafe thing to do. But my question is really a more technical one.

Am missing something that makes TCO like this a bad idea (or a risky one) for C#. Or is there something that makes it particularly tricky to get right? That is really what I am hoping to understand. Any insight?

EDIT: Thanks for the great information. I just wanted to be clear that I am not criticizing the lack of or even demanding this feature. I am not super interested in the rational around prioritization. My curiosity is what obstacles might I not perceive or understand that make this a difficult, dangerous, or undesirable thing to do.

Perhaps a different context will help focus the conversation...

Let's say I was going to implement my own C#-like language on the CLR. Why would I not (other than opportunity cost) include automatic and transparent emission of the 'tail.' instruction where appropriate? What challenges would I encounter or what constraints would I introduce in supporting this feature in a language very much like C#.

Thank you again (and in advance) for the responses.

Community
  • 1
  • 1
Justin
  • 8,853
  • 4
  • 42
  • 42
  • Just a blog post for reference - not an actual answer: http://blogs.msdn.com/b/davbr/archive/2007/06/20/tail-call-jit-conditions.aspx – Jon Skeet Aug 17 '11 at 16:23
  • http://blogs.msdn.com/b/ericlippert/archive/2009/06/22/why-doesn-t-c-implement-top-level-methods.aspx – Hans Passant Aug 17 '11 at 16:27
  • @Hans - Thank you. From your link, the idea that top-level methods go against the design of a component-based language is a good example of a technical (or at least design) issue. That is the kind of issue, if there is one, that I was hoping to uncover as a reason for not emitting 'tail.' instructions from C#. Recursion does not seem out-of-step with the design or direction of C# (LINQ, lambdas, closures, anonymous types, etc). That nobody has gotten around to it (or seen it as a priority) is another matter. – Justin Aug 17 '11 at 18:22
  • That wasn't the point of the link. The point is that somebody has to design and implement it. These things don't happen automagically and even a company as large as Microsoft has to make choices. – Hans Passant Aug 17 '11 at 18:26
  • @Hans - Ok. I guess that is why I said "I get that it has historically maybe just not been worth it" in my original question. I tried pretty hard to indicate that I was looking for technical constraints rather than resource allocation concerns. In the context of my question, I am do not understand the point of your comment. – Justin Aug 17 '11 at 18:30

2 Answers2

13

check the following links

Why doesn't .NET/C# optimize for tail-call recursion? /491463#491463
http://social.msdn.microsoft.com/Forums/en-US/netfxtoolsdev/thread/67b6d908-8811-430f-bc84-0081f4393336?StatusCode=1
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=166013&wa=wsignin1.0

The following statement is MS official (Luke Hoban Visual C# Compiler Program Manager) and copied from last link

Thanks for the suggestion. We've considered emiting tail call instructions at a number of points in the development of the C# compiler. However, there are some subtle issues which have pushed us to avoid this so far: 1) There is actually a non-trivial overhead cost to using the .tail instruction in the CLR (it is not just a jump instruction as tail calls ultimately become in many less strict environments such as functional language runtime environments where tail calls are heavily optimized). 2) There are few real C# methods where it would be legal to emit tail calls (other languages encourage coding patterns which have more tail recursion, and many that rely heavily on tail call optimization actually do global re-writing (such as Continuation Passing transformations) to increase the amount of tail recursion). 3) Partly because of 2), cases where C# methods stack overflow due to deep recursion that should have succeeded are fairly rare.

All that said, we continue to look at this, and we may in a future release of the compiler find some patterns where it makes sense to emit .tail instructions.

Community
  • 1
  • 1
Yahia
  • 69,653
  • 9
  • 115
  • 144
  • Edited to use the quote format. Please use four spaces for code, and the quote format (`> `) for quotes. – orlp Aug 17 '11 at 16:27
  • 1
    thanks - didn't know that... always something to learn :-) – Yahia Aug 17 '11 at 16:28
  • 1
    @Yahia-Thanks much. My read of the quote is that 1) The JIT might suck 2) We do not use it 3) Nobody else uses it much either. These all seem somewhat chicken-and-egg or even self-fulfilling. These points also seem more debatable with .NET 4 and beyond. At any rate, these are good justifications for not bothering but they do not sound like real technical obstacles from the compiler point of view. – Justin Aug 17 '11 at 18:24
  • @Justin: to me the implementation is rather complex to get right AND esaily understood/usable... so I think with all the features MS has to juggle in .NET etc. this might be a wise decision on resource usage regarding this specific feature. – Yahia Aug 17 '11 at 18:28
8

Good question. I don't have a concrete answer, but I have some thoughts you might find interesting. (I've been told before that I shouldn't post such things as answers, but hey...) Yahia's answer looks like the most definitive one you're likely to get, although I'll also ping Eric Lippert to see if he wants to chime in.

Part of the blog post Hans linked to in the comments will probably cover some of his thoughts, but I'm sure there are more:

I am asked "why doesn't C# implement feature X?" all the time. The answer is always the same: because no one ever designed, specified, implemented, tested, documented and shipped that feature. All six of those things are necessary to make a feature happen. All of them cost huge amounts of time, effort and money. Features are not cheap, and we try very hard to make sure that we are only shipping those features which give the best possible benefits to our users given our constrained time, effort and money budgets.


Now back to my own thoughts:

I suspect it's just never been a priority, but there's a good reason not to be too gung-ho about it: if developers are going to rely on it, it needs to be guaranteed. You wrote:

So, it would be nice if using recursion was not such an unsafe thing to do. But my question is really a more technical one.

Now, look at the blog post explaining when tail call optimizations are applied. That was back in 2007, and it explicitly states:

Note that these statements apply to the JITs as they were when Grant and Fei looked through the code base, and are prone to change at whim. You must not take dependencies on this behavior. Use this information for your own personal entertainment only.

There's then a long list of conditions which are required before tail calls will be applied. Plenty of those are non-trivial to guess from a coder's point of view.

You're absolutely right that it would be nice if using recursion was safe in certain circumstances - but I believe that's only the case if it could be guaranteed to be safe. If it were safe in 95% of cases but the 5% were hard to predict, we'd have a whole slew of "works on my machine" bugs.

What I would like is for the C# language to allow me to state a requirement of tail call optimization. The compiler could then verify that it's correct, and ideally provide more than just a hint to the JIT that this behaviour is required. Basically, if I'm going to recurse in a somewhat-uncontrolled manner, I'd better know it's going to work. Can you imagine if garbage collection were only enabled in some configurations? Eek!

So +1 to the idea of tail recursion being a useful concept, but I think we need more support from the language, the JIT and the compiler before it could truly be considered "safe".

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • I see that answer a lot about how your team is small, features are complicated, etc. Do you think that the lack of a community process hurts you here? I know that many other languages get some user-wanted features more or less designed for free by change proposals. – Rag Aug 17 '11 at 17:42
  • Jon - You honour me with your reply. I just wanted to point out that we have the "works on my machine" bug problem today as the implementation and application of TCO varies (eg. .NET 4 vs earlier and 64-bit vs. 32-bit). If C# emitted the 'tail.' instruction we would just have "works on a whole lot more machines" bugs instead. Actually, the list from that blog post is mostly stuff that would either work all the time or none of the time. So if 'tail.' was present we would probably have a lot fewer "works on my machine" bugs than we have today. "il did not have tail. prefix" is the big variable. – Justin Aug 17 '11 at 17:52
  • 1
    Your musings are, as usual, spot on. And yes, the quoted response from LukeH in Yahia's answer sums up our position nicely. – Eric Lippert Aug 17 '11 at 18:02
  • @Brian: If that was to me, I'm flattered that you think I'm on the C# team - but actually I work for Google, on Google Offers. I'm only an *amateur* C# developer. Quite a keen amateur, admittedly... – Jon Skeet Aug 17 '11 at 18:04
  • Haha, OK, sorry. @Eric Same question! :) – Rag Aug 17 '11 at 18:09
  • 1
    @Brian: First, I should correct any possible misapprehension about our community processes. We have a variety of levels of formality in our community processes. On the informal side we have people like me who write blogs, answer StackOverflow questions, read forum questions, and so on, and we get great user feedback from that. On a somewhat more formal level we have invitation-only groups of MVPs and other community luminaries who help us prioritize possible features and review designs. I feel that we are served well by our existing community processes. – Eric Lippert Aug 17 '11 at 18:15
  • 1
    @Brian: That said, nothing comes "for free"; the cost of getting "free" design suggestions is slogging through the 99% of them that have more cost than benefit. The design cost is a fairly large factor in our costs, but frankly the biggest cost is usually QA, and that's not something we want to outsource to our customers. :-) – Eric Lippert Aug 17 '11 at 18:19
  • Note that the JIT tail call guarantees were greatly improved in the the CLR v4.0: http://blogs.msdn.com/b/clrcodegeneration/archive/2009/05/11/tail-call-improvements-in-net-framework-4.aspx. If the .tail prefix is used, the JIT will basically always honor it now, with a few understandable exceptions (e.g. needing to keep stack frames for security reasons when calling across trust boundaries). – kvb Aug 17 '11 at 19:09
  • 1
    @kvb-Amazing. Thanks. If this was an answer it would be the one I would accept. Not only does this have great detail but it actually explains a few cases where the compiler would be better off not emitting 'tail.'. That said, it seems that even now only x64 really offers any level of guarantee that 'tail.' will be honoured. – Justin Aug 17 '11 at 21:33
  • @Justin - that blog post mainly deals with the x64 JIT, but that's because the x64 JIT frequently didn't honor the .tail prefix in .NET 2.0. I haven't seen anything definitive but my impression is that due to calling convention differences honoring the .tail prefix in the x86 JIT was never much of an issue even in .NET 2.0. – kvb Aug 18 '11 at 01:06
  • @kvb-Your link links to the following: http://blogs.msdn.com/b/davbr/archive/2007/06/20/tail-call-jit-conditions.aspx ... On that page, check out where it says "Fei has this to add about the 32-bit JIT". That seems to imply that the 32 bit JIT also has trouble honouring the "tail." instruction. Your main link does say though that "The 32-bit JIT had a general purpose tail call mechanism that always worked, but wasn’t performant enough to consider it an optimization, so it was only used when the IL requested a tail call." ia64 seems to have been left out in the cold either way. – Justin Aug 18 '11 at 11:26