可能重复:
为什么 .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# 的语言中支持此功能时,我会遇到哪些挑战或会引入哪些限制。
再次(并提前)感谢您的回复。