Why doesn't .NET/C# optimize for tail-call recursion?
我发现了关于哪些语言优化尾部递归的问题。为什么C不尽可能优化尾部递归?
对于具体情况,为什么不将此方法优化为循环(如果这很重要,则为Visual Studio 2008 32位)?:
1 2 3 4 5 6 7 8 9 10 | 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将生成相关的操作码(尽管对于简单的递归,它可能只是将整个过程直接转换为
有关一些详细信息,请参阅此博客文章(考虑到最近的JIT更改,现在很可能已经过时)。请注意,clr对4.0的更改x86、x64和ia64将尊重它。
此Microsoft Connect反馈提交应回答您的问题。它包含了微软的官方回应,所以我建议你按这个去做。
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.
顺便说一下,正如已经指出的,值得注意的是尾部递归在X64上进行了优化。
C不优化尾调用递归,因为这就是F的用途!
有关阻止C编译器执行尾调用优化的条件的一些深度,请参阅本文:jit clr尾调用条件。
C和F之间的互操作性#
C和F的互操作性非常好,而且由于.NET公共语言运行库(clr)的设计考虑到了这种互操作性,因此每种语言都设计有针对其意图和目的的优化。对于显示从C代码调用F代码有多容易的示例,请参见从C代码调用F代码;对于从F代码调用C函数的示例,请参见从F代码调用C函数。
有关委托互操作性,请参阅本文:F、C和Visual Basic之间的委托互操作性。
C和F之间的理论和实践差异#
这里有一篇文章介绍了一些区别,并解释了C和F之间的尾调用递归的设计差异:在C和F中生成尾调用操作码。
这里有一篇文章,其中有C,f,c++CLI中的一些例子:C语言中的尾部递归的冒险,F**和C++CLI。
理论上的主要区别在于C_是用循环设计的,而F_是根据lambda微积分原理设计的。关于lambda微积分原理的非常好的书,请看这本免费的书:Abelson,Sussman和Sussman的《计算机程序的结构和解释》。
有关f中尾调用的非常好的介绍性文章,请参阅本文:f中尾调用的详细介绍。最后,这里有一篇文章介绍了非尾递归和尾调用递归(在f)之间的区别:尾递归和f sharp中的非尾递归。
最近有人告诉我,64位的C编译器确实优化了尾部递归。
C也实现了这一点。之所以不总是应用它,是因为用于应用尾部递归的规则非常严格。
您可以使用蹦床技术来处理C语言(或Java)中的尾递归函数。但是,更好的解决方案(如果您只关心堆栈利用率)是使用这个小助手方法包装相同递归函数的部分,并使其迭代,同时保持函数的可读性。