可能的重复:
为什么.net/C# 不消除尾递归? https://stackoverflow.com/questions/491376/why-doesnt-net-c-eliminate-tail-recursion
采取以下 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# 来说是一个坏主意(或者是一个有风险的主意)。或者是否有什么事情让它变得特别棘手?这确实是我希望了解的。有什么见解吗?
EDIT:感谢您提供的宝贵信息。我只是想澄清,我并不是在批评这项功能的缺乏,甚至不是要求这项功能。我对优先级的合理性不太感兴趣。我的好奇心是我可能无法察觉或理解哪些障碍,使这成为一件困难、危险或不受欢迎的事情。
也许不同的背景将有助于集中对话......
假设我要在 CLR 上实现我自己的类似 C# 的语言。为什么我不(除了机会成本)包括自动和透明的“尾部”排放。在适当的地方进行指导?在使用类似于 C# 的语言支持此功能时,我会遇到哪些挑战或会引入哪些限制。
再次(提前)感谢您的回复。