Tail Call Opcode generieren
Aus Neugier versuchte ich mit C # einen Tail Call Opcode zu generieren. Fibinacci ist einfach, also sieht mein C # -Beispiel so aus:
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);
}
Wenn ich es in Release baue und ohne Debugging ausführe, bekomme ich keinen Stapelüberlauf. Beim Debuggen oder Ausführen ohne Optimierungen tritt ein Stapelüberlauf auf, was bedeutet, dass der Tail-Aufruf bei Release mit Optimierungen funktioniert (was ich erwartet hatte).
Die MSIL dafür sieht so aus:
.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
}
Ich hätte erwartet, einen Tail Opcode zu sehenmsdn, aber es ist nicht da. Ich habe mich gefragt, ob der JIT-Compiler dafür verantwortlich ist, es dort abzulegen. Ich habe versucht, die Baugruppe zu ngen (mitngen install <exe>
, navigieren Sie zur Windows-Assembly-Liste (um sie abzurufen) und laden Sie sie erneut in ILSpy, aber für mich sieht es genauso aus:
.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
}
Ich sehe es immer noch nicht.
Ich weiß, dass F # den Tail-Call gut handhabt, also wollte ich vergleichen, was F # mit dem tat, was C # tat. Mein F # -Beispiel sieht so aus:
let rec fibb i acc =
if i = 0 then
acc
else
fibb (i-1) (acc + i)
Console.WriteLine (fibb 3 0)
Und die generierte IL für die fib-Methode sieht folgendermaßen aus:
.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
}
Was laut ILSpy gleichbedeutend ist mit:
[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;
}
Also hat F # einen Tail Call mit goto Anweisungen erzeugt? Das habe ich nicht erwartet.
Ich versuche mich nirgendwo auf Tail Call zu verlassen, aber ich bin nur neugierig, wo genau dieser Opcode gesetzt wird. Wie macht C # das?