Из любопытства я пытался сгенерировать код операции хвоста с помощью C#. Fibinacci является легким, так что мой 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 сборку (с использованием ngen install <exe>
, навигации по списку окна сборками, чтобы получить его) и загрузить его обратно в 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)
И генерируемый 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# делает это?
Я не верю, что C# когда-либо делает оптимизацию хвостового вызова – cthom06
Похоже, что JIT оптимизирует опцию хвостового вызова, что немного странно, учитывая, что фактическое дерево вызовов используется защитой кода доступа, t ожидать, что JIT разрешено настраивать его без обозначения «хвостового хвоста» в MSIL. –
F # (как IronScheme) использует устранение хвостового вызова, чтобы изменить «дорогой» хвост на «дешевый» локальный скачок.Это делается в компиляторе. – leppie