为什么.NET / C#不能优化尾调用递归?

时间:2009-01-29 12:20:21

标签: c# .net optimization tail-recursion

我发现this question关于哪些语言优化尾递归。为什么C#不能尽可能地优化尾递归?

对于具体情况,为什么这个方法不被优化为循环(Visual Studio 2008 32位,如果这很重要)?:

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}

6 个答案:

答案 0 :(得分:77)

JIT编译是一种棘手的平衡行为,在不花费太多时间进行编译阶段(从而大大减慢短期应用程序)与不进行足够的分析以保持应用程序在长期内保持竞争力并提前达到标准之间时间编译。

有趣的是,NGen编译步骤的目的不是在优化中更积极。我怀疑这是因为他们根本不想有行为依赖于JIT或NGen是否负责机器代码的错误。

CLR本身确实支持尾调用优化,但特定于语言的编译器必须知道如何生成相关的opcode,并且JIT必须愿意尊重它。 F#'s fsc将生成相关的操作码(虽然对于简单的递归,它可能只是将整个事件直接转换为while循环)。 C#的csc没有。

有关详细信息,请参阅this blog post(鉴于最近的JIT更改,现在很可能已过时)。请注意,CLR更改为4.0 the x86, x64 and ia64 will respect it

答案 1 :(得分:71)

Microsoft Connect feedback submission应该回答您的问题。它包含了微软的官方回复,所以我建议你去做。

  

感谢您的建议。我们已经   考虑发出尾调用   在许多地方的指示   C#编译器的开发。   但是,有一些微妙的问题   这促使我们避免这样做   远:1)实际上有一个   使用的非常重要的开销成本   CLR中的.tail指令(它是   不只是作为尾部的跳转指令   电话最终变得更少   严格的环境,如功能   语言运行时环境在哪里   尾部调用经过大量优化)。 2)   它的实际C#方法很少   发出尾调用是合法的   (其他语言鼓励编码   有更多尾巴的图案   递归,以及许多严重依赖   关于尾调用优化实际上做了   全球重写(如   继续传递转换)   增加尾巴量   递归)。 3)部分原因是2),   C#方法堆栈溢出的情况   由于应该有的深度递归   成功是相当罕见的。

     

所有这一切,我们继续关注   这个,我们可能在将来发布   编译器找到一些模式   发出.tail有意义的地方   指令。

顺便说一下,正如已经指出的那样,值得注意的是在x64上优化尾递归

答案 2 :(得分:14)

C#不优化尾调用递归,因为这就是F#的用途!

对于阻止C#编译器执行尾部调用优化的条件,请参阅此文章:JIT CLR tail-call conditions

C#和F#之间的互操作性

C#和F#可以很好地互操作,并且因为.NET公共语言运行时(CLR)在设计时充分考虑了这种互操作性,所以每种语言都设计有特定于其意图和目的的优化。有关显示从C#代码调用F#代码是多么容易的示例,请参阅Calling F# code from C# code;有关从F#代码调用C#函数的示例,请参阅Calling C# functions from F#

对于委托互操作性,请参阅此文:Delegate interoperability between F#, C# and Visual Basic

C#和F#之间的理论和实践差异

这篇文章介绍了一些差异,并解释了C#和F#之间尾调用递归的设计差异:Generating Tail-Call Opcode in C# and F#

这篇文章包含C#,F#和C ++ \ CLI中的一些示例:Adventures in Tail Recursion in C#, F#, and C++\CLI

主要的理论差异是C#是用循环设计的,而F#是根据Lambda演算的原理设计的。关于Lambda演算原理的一本非常好的书,请参阅这本免费书:Structure and Interpretation of Computer Programs, by Abelson, Sussman, and Sussman

有关F#尾部调用的非常好的介绍性文章,请参阅此文章:Detailed Introduction to Tail Calls in F#。最后,这篇文章介绍了非尾递归和尾调用递归(在F#中)之间的区别:Tail-recursion vs. non-tail recursion in F sharp

答案 3 :(得分:8)

最近我被告知64位的C#编译器确实优化了尾递归。

C#也实现了这一点。它并不总是被应用的原因是用于应用尾递归的规则非常严格。

答案 4 :(得分:3)

您可以在C#(或Java)中使用trampoline technique作为尾递归函数。但是,更好的解决方案(如果您只关心堆栈利用率)是使用this small helper方法来包装相同递归函数的部分并使其迭代,同时保持函数可读。

答案 5 :(得分:0)

正如提到的其他答案一样,CLR确实支持尾部调用优化,并且从历史上看似乎正在逐步改进中。但是在C#中支持它在git存储库中存在一个开放的Proposal问题,用于设计C#编程语言Support tail recursion #2544

您可以在此处找到一些有用的详细信息。例如提到的@jaykrell

  

让我提供我所知道的。

     

有时候,尾叫是一种双赢的表现。可以节省CPU。 jmp是   比call / ret更便宜它可以节省堆栈。减少堆叠量   更好的位置。

     

有时候,尾叫会导致性能下降,赢得堆栈。   CLR具有复杂的机制,可以向其中传递更多参数   被访者比来电者要多。我的意思是说更多的堆栈   参数空间。太慢了但是它可以节省堆栈。它会   只能用尾巴做。前缀。

     

如果调用方参数是   堆栈大于被调用者参数,通常很容易实现双赢   转变。可能存在诸如参数位置更改之类的因素   从托管到整数/浮动,并生成精确的StackMap和   这样的。

     

现在,存在另一个角度,即需要算法的角度   消除尾音,以便能够处理   具有固定/小堆栈的任意大数据。这不是关于   性能,但完全可以运行。

我还要提及(作为附加信息),当我们在System.Linq.Expressions命名空间中使用表达式类生成编译的lambda时,有一个名为“ tailCall”的参数,如其注释中所述

  

布尔值,指示在编译创建的表达式时是否将应用尾部调用优化。

我还没有尝试过,我不确定它可以如何帮助解决您的问题,但是可能有人可以尝试它,并且在某些情况下可能会有用:


var myFuncExpression = System.Linq.Expressions.Expression.Lambda<Func< … >>(body: … , tailCall: true, parameters: … );

var myFunc =  myFuncExpression.Compile();