Generar código de llamada de cola

Por curiosidad, estaba intentando generar un código de operación de llamada de cola usando C #. Fibinacci es fácil, así que mi ejemplo de c # se ve así:

    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);
    }

Si lo compilo en versión y lo ejecuto sin depurar, no obtengo un desbordamiento de pila. Lo depuro o lo ejecuto sin optimizaciones y obtengo un desbordamiento de pila, lo que implica que la llamada de cola está funcionando cuando en la versión con optimizaciones activadas (que es lo que esperaba).

La MSIL para esto se ve así:

.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 
}

Hubiera esperado ver un código de operación de cola, según elmsdn, pero no está ahí. Esto me hizo preguntarme si el compilador JIT fue el responsable de ponerlo allí. Traté de ngen la asamblea (usandongen install <exe>, navegando a la lista de ensamblajes de Windows para obtenerlo y cargarlo de nuevo en ILSpy pero a mí me parece lo mismo:

.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 
}

Todavía no lo veo.

Sé que F # maneja bien la llamada de cola, así que quería comparar lo que F # hizo con lo que C # hizo. Mi ejemplo de F # se ve así:

let rec fibb i acc =  
    if i = 0 then
        acc
    else 
        fibb (i-1) (acc + i)


Console.WriteLine (fibb 3 0)

Y el IL generado para el método de fib se ve así:

.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
}

Lo que, según ILSpy, es equivalente a esto:

[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;
}

Entonces, ¿F # generó una llamada de cola usando instrucciones goto? Esto no es lo que esperaba.

No estoy tratando de confiar en la llamada de cola en cualquier lugar, pero tengo curiosidad por saber exactamente dónde se establece ese código de operación. ¿Cómo está haciendo C # esto?

Respuestas a la pregunta(3)

Su respuesta a la pregunta