出于好奇我试图使用C#生成一个尾调用操作码。 斐波那契数列是一个简单的例子,因此我的C#示例看起来像这样:
private static void Main(string[] args)
{
Console.WriteLine(Fib(int.MaxValue, 0));
}
public static int Fib(int i, int acc)
{
if (i == 0)
{
return acc;
}
return Fib(i - 1, acc + i);
}
如果我使用发布版本构建并在不进行调试的情况下运行它,我就不会遇到堆栈溢出。但如果进行调试或者没有进行优化运行时,我就会遇到堆栈溢出,这表明尾递归在开启优化的发布版本中起作用(这是我预期的)。
这段代码的 MSIL 如下:
.method public hidebysig static int32 Fib(int32 i, int32 acc) cil managed
{
// Method Start RVA 0x205e
// Code Size 17 (0x11)
.maxstack 8
L_0000: ldarg.0
L_0001: brtrue.s L_0005
L_0003: ldarg.1
L_0004: ret
L_0005: ldarg.0
L_0006: ldc.i4.1
L_0007: sub
L_0008: ldarg.1
L_0009: ldarg.0
L_000a: add
L_000b: call int32 [ConsoleApplication2]ConsoleApplication2.Program::Fib(int32,int32)
L_0010: ret
}
根据MSDN,我本来期望看到一个尾部操作码,但是没有。这让我想知道JIT编译器是否有责任将其放入其中?我尝试使用ngen install <exe>
对程序集进行ngened(导出原生映像),并在ILSpy中重新加载它,但结果对我来说看起来是一样的:
.method public hidebysig static int32 Fib(int32 i, int32 acc) cil managed
{
// Method Start RVA 0x3bfe
// Code Size 17 (0x11)
.maxstack 8
L_0000: ldarg.0
L_0001: brtrue.s L_0005
L_0003: ldarg.1
L_0004: ret
L_0005: ldarg.0
L_0006: ldc.i4.1
L_0007: sub
L_0008: ldarg.1
L_0009: ldarg.0
L_000a: add
L_000b: call int32 [ConsoleApplication2]ConsoleApplication2.Program::Fib(int32,int32)
L_0010: ret
}
我还是看不懂。
我知道 F# 很好地处理了尾递归,所以我想比较一下 F# 和 C# 的做法。我的 F# 示例如下:
let rec fibb i acc =
if i = 0 then
acc
else
fibb (i-1) (acc + i)
Console.WriteLine (fibb 3 0)
而fib方法生成的中间语言(IL)如下所示:
.method public static int32 fibb(int32 i, int32 acc) cil managed
{
// Method Start RVA 0x2068
// Code Size 18 (0x12)
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute::.ctor(int32[]) = { int32[](Mono.Cecil.CustomAttributeArgument[]) }
.maxstack 5
L_0000: nop
L_0001: ldarg.0
L_0002: brtrue.s L_0006
L_0004: ldarg.1
L_0005: ret
L_0006: ldarg.0
L_0007: ldc.i4.1
L_0008: sub
L_0009: ldarg.1
L_000a: ldarg.0
L_000b: add
L_000c: starg.s acc
L_000e: starg.s i
L_0010: br.s L_0000
}
根据ILSpy,这相当于: 这个
[Microsoft.FSharp.Core.CompilationArgumentCounts(Mono.Cecil.CustomAttributeArgument[])]
public static int32 fibb(int32 i, int32 acc)
{
label1:
if !(((i != 0)))
{
return acc;
}
(i - 1);
i = acc = (acc + i);;
goto label1;
}
所以F#使用goto语句生成尾递归?这不是我预期的。
我并没有试图在任何地方依赖尾递归,但我只是好奇该操作码是在哪里设置的?C#是如何做到这一点的?
tail.
调用的生成也取决于您是为x86还是x64编译。上次我读到关于这个问题的时候,当针对x64时,尾调用优化无法工作。 - stakx - no longer contributing