C# 不发出“尾部”是否存在技术原因? CIL指令?
可能的重复:
为什么.net/C#不消除尾递归?
以下 C# 代码:
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);
}
}
}
C# 编译器(无论如何)会将 Counter 方法编译为以下 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
}
上面代码的问题是它会导致堆栈溢出(在我的硬件上大约为 i=262000)。为了解决这个问题,一些语言采取了所谓的尾部调用消除或尾部调用优化(TCO)的方法。本质上,它们将递归调用变成了循环。
我的理解是 .NET 4 JIT 的 64 位实现现在执行 TCO 并避免像上面的 CIL 那样的代码溢出。然而,32 位 JIT 则不然。 Mono 似乎也没有。有趣的是,JIT(面临时间和资源压力)会执行 TCO,而 C# 编译器则不会。我的问题是为什么 C# 编译器本身不更了解 TCO?
有一条 CIL 指令告诉 JIT 执行 TCO。例如,C# 编译器可以生成以下 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
}
与原始代码不同,此代码不会溢出,并且即使在 32 位 JIT(.NET 和 Mono)上也能运行完成。神奇之处在于 tail.
CIL 指令。像 F# 这样的编译器将自动生成包含该指令的 CIL。
所以我的问题是,C# 编译器不这样做是否有技术原因?
我知道从历史上看这可能是不值得的。像 Counter()
这样的代码在惯用的 C# 和/或 .NET 框架中并不常见。您可以轻松地将 C# 的 TCO 视为不必要的或过早的优化。
随着 LINQ 和其他东西的引入,C# 和 C# 开发人员似乎都在向功能性更强的方向发展。因此,如果使用递归不是一件不安全的事情,那就太好了。但我的问题确实是一个更具技术性的问题。
我错过了一些东西,使得这样的 TCO 对于 C# 来说是一个坏主意(或者是一个有风险的主意)。或者是否有什么事情使得做对变得特别困难?这确实是我希望了解的。有什么见解吗?
编辑:感谢您提供的重要信息。我只是想澄清,我并不是在批评这项功能的缺乏,甚至不是要求这项功能。我对优先级的合理性不太感兴趣。我的好奇心是我可能无法察觉或理解哪些障碍,使这成为一件困难、危险或不受欢迎的事情。
也许不同的上下文将有助于集中讨论...
假设我要在 CLR 上实现我自己的类似 C# 的语言。为什么我不(除了机会成本)包括自动和透明的“尾部”排放。在适当的地方进行指导?在使用类似于 C# 的语言支持此功能时,我会遇到哪些挑战或会引入哪些限制。
再次(提前)感谢您的回复。
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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
检查以下链接
为什么 .NET/C# 不针对 tail 进行优化-调用递归?
/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
以下说法为MS官方(Luke Hoban Visual C# 编译器程序管理器)并从上一个链接复制
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
好问题。我没有具体的答案,但我有一些你可能会感兴趣的想法。 (之前有人告诉我,我不应该发布答案之类的东西,但是嘿......)Yahia 的答案看起来像是你可能得到的最明确的答案,尽管我也会 ping Eric Lippert 看看是否他想插话。
博客文章 Hans 链接评论中的 to 可能会涵盖他的一些想法,但我确信还有更多:
现在回到我自己的想法:
我怀疑它从来都不是一个优先事项,但有一个很好的理由不要对此过于狂热:如果开发人员要依赖它,它需要得到保证。你写道:
现在,查看 博客文章,解释何时进行尾调用优化已应用。那是在 2007 年,它明确指出:
在应用尾部调用之前需要满足一长串条件。从编码员的角度来看,其中很多都不是简单的猜测。
如果在某些情况下使用递归是安全的,那就太好了,这是绝对正确的 - 但我相信只有在可以保证安全的情况下才是这种情况。如果 95% 的情况下是安全的,但 5% 的情况很难预测,那么我们就会遇到大量“在我的机器上运行”的错误。
我希望 C# 语言允许我陈述尾调用优化的要求。然后,编译器可以验证它是否正确,并且理想情况下,不仅仅是向 JIT 提供需要此行为的提示。基本上,如果我要以某种不受控制的方式递归,我最好知道它会起作用。您能想象如果仅在某些配置中启用垃圾收集吗?哎呀!
因此,尾递归是一个有用的概念,但我认为我们需要语言、JIT 和编译器的更多支持,然后才能真正被认为是“安全”的。
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:
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:
Now, look at the blog post explaining when tail call optimizations are applied. That was back in 2007, and it explicitly states:
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".