为什么.NET / C#没有针对tail-callrecursion进行优化?

我发现这个问题关于哪些语言优化尾recursion。 为什么C#不尽可能地优化尾recursion?

对于一个具体的情况,为什么不把这个方法优化成一个循环( Visual Studio 2008 32位,如果有的话)?

private static void Foo(int i) { if (i == 1000000) return; if (i % 100 == 0) Console.WriteLine(i); Foo(i+1); } 

JIT编译是一个棘手的平衡行为,不需要花费太多的时间来完成编译阶段(从而大大减缓了短期应用程序),也没有进行足够的分析来保持应用程序的长期竞争力,而且提供了标准的提前编译。

有趣的是, NGen编译步骤并不是针对在优化方面更积极。 我怀疑这是因为他们根本就不想在行为取决于JIT或NGen是否对机器代码负责。

CLR本身支持尾部调用优化,但是特定于语言的编译器必须知道如何生成相关的操作码 ,并且JIT必须愿意尊重它。 F#的 fsc将生成相关的操作码(尽pipe对于简单的recursion来说,它可能直接将整个事物转换为while循环)。 C#的csc没有。

看到这个博客文章的一些细节(很可能现在已经过时了最近的JIT变化)。 请注意,CLR更改为4.0 的x86,x64和ia64将尊重它 。

此Microsoft连接反馈提交应回答您的问题。 它包含了来自微软的官方回应,所以我build议通过这个。

感谢您的build议。 我们已经考虑在C#编译器开发中的许多要点上发出尾部调用指令。 但是,有一些细微的问题促使我们避免这个问题:1)在CLR中使用.tail指令实际上有一个不重要的开销成本(它不仅仅是一个跳转指令,因为尾调用最终成为在很多不太严格的环境中,例如function语言运行时环境,其中尾部调用被大量优化)。 2)很less有真正的C#方法可以合法地发出尾调用(其他语言鼓励具有更多尾recursion的编码模式,并且很多很大程度上依赖于尾调用优化的实际上是全局重写(如Continuation Passing转换)来增加尾recursion量)。 3)部分原因在于2),由于深recursion应该已经成功的C#方法堆栈溢出的情况相当less见。

所有这一切,我们继续看这个,我们可能在未来的编译器发现一些模式,发出.tail指令是有意义的。

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

C#没有针对尾部recursion进行优化,因为这是F#的用途!

有关阻止C#编译器执行尾部调用优化的条件的深入信息,请参阅此文章: JIT CLR尾部调用条件 。

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

C#和F#互操作性非常好,而且由于.NET公共语言运行时(CLR)在devise时考虑到了这种互操作性,因此每种语言的devise都是针对其意图和目的进行的优化。 有关示例显示从C#代码调用F#代码有多容易的示例,请参阅从C#代码调用F#代码 ; 有关从F#代码调用C#函数的示例,请参阅从F# 调用C#函数 。

有关委托互操作性,请参阅此文章: 委托F#,C#和Visual Basic之间的互操作性 。

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

下面是一篇介绍一些差异的文章,并解释了C#和F#之间的尾部recursion的devise差异:在C#和F#中生成尾部调用操作码 。

下面是一些在C#,F#和C ++ \ CLI中的例子:C#,F#和C ++ \ CLI 中的尾recursion冒险

主要的理论差异在于C#被devise为循环,而F#被devise为基于Lambda演算的原则。 关于Lambda微积分原理的一本很好的书,请参阅Abelson,Sussman和Sussman的这本免费书籍: 计算机程序的结构和解释 。

有关F#中尾部调用的非常好的介绍性文章,请参阅本文: F#中尾部调用的详细介绍 。 最后,这里是一篇文章,介绍了非尾recursion和尾调用recursion(在F#中)的区别: F sharp中的尾recursion与非尾recursion 。

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

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

您可以使用蹦床技术在C#(或Java)中进行尾recursion函数。 然而,更好的解决scheme(如果你只关心栈的利用率)就是使用这个小的帮助器方法来包装同一个recursion函数的一部分,并使其迭代,同时保持函数的可读性。