为什么 .NET/C# 不优化尾调用递归?
我发现这个问题关于哪些语言优化尾递归。 为什么 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);
}
I found this question about which languages optimize tail recursion. Why C# doesn't optimize tail recursion, whenever possible?
For a concrete case, why isn't this method optimized into a loop (Visual Studio 2008 32-bit, if that matters)?:
private static void Foo(int i)
{
if (i == 1000000)
return;
if (i % 100 == 0)
Console.WriteLine(i);
Foo(i+1);
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
JIT 编译是一种棘手的平衡行为,既不花太多时间进行编译阶段(从而大大减慢短期应用程序的速度),又不进行足够的分析以通过标准提前编译保持应用程序的长期竞争力。 。
有趣的是,NGen 编译步骤并不是为了更积极地进行优化。 我怀疑这是因为他们根本不希望出现错误,其中行为取决于 JIT 还是 NGen 是否负责机器代码。
CLR 本身确实支持尾调用优化,但特定于语言的编译器必须知道如何生成相关 操作码 和 JIT 必须愿意尊重它。
F#'s fsc 将生成相关的操作码(尽管对于简单的递归,它可能只是将整个事情直接进入
while
循环中)。 C# 的 csc 没有。请参阅此博文了解一些详细信息(很可能现在已经过时了)考虑到最近的 JIT 变化)。 请注意,4.0 的 CLR 发生了变化 x86、x64 和 ia64 将尊重它。
JIT compilation is a tricky balancing act between not spending too much time doing the compilation phase (thus slowing down short lived applications considerably) vs. not doing enough analysis to keep the application competitive in the long term with a standard ahead-of-time compilation.
Interestingly the NGen compilation steps are not targeted to being more aggressive in their optimizations. I suspect this is because they simply don't want to have bugs where the behaviour is dependent on whether the JIT or NGen was responsible for the machine code.
The CLR itself does support tail call optimization, but the language-specific compiler must know how to generate the relevant opcode and the JIT must be willing to respect it.
F#'s fsc will generate the relevant opcodes (though for a simple recursion it may just convert the whole thing into a
while
loop directly). C#'s csc does not.See this blog post for some details (quite possibly now out of date given recent JIT changes). Note that the CLR changes for 4.0 the x86, x64 and ia64 will respect it.
这Microsoft Connect 反馈提交应该可以回答您的问题。 它包含 Microsoft 的官方回复,因此我建议您遵循该回复。
顺便说一句,正如已经指出的那样,值得注意的是尾递归在 x64 上进行了优化。
This Microsoft Connect feedback submission should answer your question. It contains an official response from Microsoft, so I'd recommend going by that.
By the way, as it has been pointed out, it is worth noting that tail recursion is optimised on x64.
C# 不会针对尾部调用递归进行优化,因为这正是 F# 的用途!
有关阻止 C# 编译器执行尾调用优化的条件的深入信息,请参阅本文:JIT CLR 尾部调用条件。
C# 和 F# 之间的互操作性
C# 和 F# 的互操作性非常好,并且由于 .NET 公共语言运行时 (CLR) 在设计时就考虑到了这种互操作性,因此每种语言都根据其特定意图进行了优化和目的。 有关说明从 C# 代码调用 F# 代码是多么容易的示例,请参阅调用 F#来自 C# 代码的代码; 有关从 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 、萨斯曼和萨斯曼。
有关 F# 尾部调用的非常好的介绍性文章,请参阅这篇文章:F# 中尾部调用详细介绍。 最后,这里有一篇文章介绍了非尾递归和尾调用递归(在 F# 中)之间的区别:F 升调中的尾递归与非尾递归。
C# does not optimize for tail-call recursion because that's what F# is for!
For some depth on the conditions that prevent the C# compiler from performing tail-call optimizations, see this article: JIT CLR tail-call conditions.
Interoperability between C# and F#
C# and F# interoperate very well, and because the .NET Common Language Runtime (CLR) is designed with this interoperability in mind, each language is designed with optimizations that are specific to its intent and purposes. For an example that shows how easy it is to call F# code from C# code, see Calling F# code from C# code; for an example of calling C# functions from F# code, see Calling C# functions from F#.
For delegate interoperability, see this article: Delegate interoperability between F#, C# and Visual Basic.
Theoretical and practical differences between C# and F#
Here is an article that covers some of the differences and explains the design differences of tail-call recursion between C# and F#: Generating Tail-Call Opcode in C# and F#.
Here is an article with some examples in C#, F#, and C++\CLI: Adventures in Tail Recursion in C#, F#, and C++\CLI
The main theoretical difference is that C# is designed with loops whereas F# is designed upon principles of Lambda calculus. For a very good book on the principles of Lambda calculus, see this free book: Structure and Interpretation of Computer Programs, by Abelson, Sussman, and Sussman.
For a very good introductory article on tail calls in F#, see this article: Detailed Introduction to Tail Calls in F#. Finally, here is an article that covers the difference between non-tail recursion and tail-call recursion (in F#): Tail-recursion vs. non-tail recursion in F sharp.
最近有人告诉我,64 位的 C# 编译器确实优化了尾递归。
C# 也实现了这一点。 之所以不总是应用它,是因为用于应用尾递归的规则非常严格。
I was recently told that the C# compiler for 64 bit does optimize tail recursion.
C# also implements this. The reason why it is not always applied, is that the rules used to apply tail recursion are very strict.
今天我有一个惊喜:-)
我正在复习我即将推出的 C# 递归课程的教材。
尾调用优化似乎终于进入了 C#。
我正在使用 NET5 和 LINQPad 6(已激活优化)。
这是我使用的 Tail 调用可优化阶乘函数:
这是为此函数生成的 X64 汇编代码:
看,没有
call
,只有一个jmp. 该函数也被积极优化(无堆栈帧设置/拆卸)。 哦是的!
I had a happy surprise today :-)
I am reviewing my teaching material for my upcoming course on recursion with C#.
And it seems that finally tail call optimization has made its way into C#.
I am using NET5 with LINQPad 6 (optimization activated).
Here is the Tail call optimizable Factorial function I used:
And here is the X64 assembly code generated for this function:
See, there is no
call
, only ajmp
. The function is agressively optimized as well (no stack frame setup/teardown). Oh Yes!您可以使用蹦床技术来实现 C# 中的尾递归函数(或Java)。 但是,更好的解决方案(如果您只关心堆栈利用率)是使用 这个小帮助器方法可以包装同一递归函数的各个部分并使其迭代,同时保持函数的可读性。
You can use the trampoline technique for tail-recursive functions in C# (or Java). However, the better solution (if you just care about stack utilization) is to use this small helper method to wrap parts of the same recursive function and make it iterative while keeping the function readable.
正如其他答案所提到的,CLR 确实支持尾部调用优化,而且历史上它似乎一直在逐步改进。 但在 C# 中支持它在 git 存储库中存在一个开放的
Proposal
问题,用于 C# 编程语言的设计 支持尾递归#2544。您可以在那里找到一些有用的详细信息和信息。 例如@jaykrell提到的
另外让我提一下(作为额外信息),当我们使用 System.Linq.Expressions 命名空间中的表达式类生成已编译的 lambda 时,有一个名为“tailCall”的参数,如其注释中所述是
我还没有尝试过,我不确定它如何帮助解决您的问题,但也许有人可以尝试它,并且在某些情况下可能有用:
As other answers mentioned, CLR does support tail call optimization and it seems it was under progressive improvements historically. But supporting it in C# has an open
Proposal
issue in the git repository for the design of the C# programming language Support tail recursion #2544.You can find some useful details and info there. For example @jaykrell mentioned
Also let me mention (as extra info), When we are generating a compiled lambda using expression classes in
System.Linq.Expressions
namespace, there is an argument named 'tailCall' that as explained in its comment it isI was not tried it yet, and I am not sure how it can help related to your question, but Probably someone can try it and may be useful in some scenarios: