Generate tail call opcode

后端 未结 3 1287
野的像风
野的像风 2020-11-28 12:04

Out of curiosity I was trying to generate a tail call opcode using C#. Fibinacci is an easy one, so my c# example looks like this:

    private static void M         


        
相关标签:
3条回答
  • 2020-11-28 12:28

    The situation with tail call optimization in .Net is quite complicated. As far as I know, it's like this:

    • The C# compiler will never emit the tail. opcode and it will also never do the tail call optimization by itself.
    • The F# compiler sometimes emits the tail. opcode and sometimes does the tail call optimization by itself by emitting IL that's not recursive.
    • The CLR will honor the tail. opcode if it's present and the 64-bit CLR will sometimes make the tail call optimization even when the opcode is not present.

    So, in your case, you didn't see the tail. opcode in the IL generated by the C# compiler, because it doesn't do that. But the method was tail-call optimized, because the CLR sometimes does that even without the opcode.

    And in the F# case, you observed that the f# compiler did the optimization by itself.

    0 讨论(0)
  • 2020-11-28 12:31

    Like all optimizations performed in .NET (Roslyn languages), tail call optimization is a job performed by the jitter, not the compiler. The philosophy is that putting the job on the jitter is useful since any language will benefit from it and the normally difficult job of writing and debugging a code optimizer has to be done only once per architecture.

    You have to look at the generated machine code to see it being done, Debug + Windows + Disassembly. With the further requirement that you do so by looking at the Release build code that's generated with the Tools + Options, Debugging, General, Suppress JIT optimization unticked.

    The x64 code looks like this:

            public static int Fib(int i, int acc) {
                if (i == 0) {
    00000000  test        ecx,ecx 
    00000002  jne         0000000000000008 
                    return acc;
    00000004  mov         eax,edx 
    00000006  jmp         0000000000000011 
                }
    
                return Fib(i - 1, acc + i);
    00000008  lea         eax,[rcx-1] 
    0000000b  add         edx,ecx 
    0000000d  mov         ecx,eax 
    0000000f  jmp         0000000000000000              // <== here!!!
    00000011  rep ret  
    

    Note the marked instruction, a jump instead of a call. That's tail call optimization at work. A quirk in .NET is that the 32-bit x86 jitter does not perform this optimization. Simply a to-do item that they'll probably never get around to. Which did require the F# compiler writers to not ignore the problem and emit Opcodes.Tailcall. You'll find other optimizations performed by the jitter documented in this answer.

    0 讨论(0)
  • 2020-11-28 12:34

    C# compiler does not give you any guarantees about tail-call optimizations because C# programs usually use loops and so they do not rely on the tail-call optimizations. So, in C#, this is simply a JIT optimization that may or may not happen (and you cannot rely on it).

    F# compiler is designed to handle functional code that uses recursion and so it does give you certain guarantees about tail-calls. This is done in two ways:

    • if you write a recursive function that calls itself (like your fib) the compiler turns it into a function that uses loop in the body (this is a simple optimization and the produced code is faster than using a tail-call)

    • if you use a recursive call in a more complex position (when using continuation passing style where function is passed as an argument), then the compiler generates a tail-call instruction that tells the JIT that it must use a tail call.

    As an example of the second case, compile the following simple F# function (F# does not do this in Debug mode to simplify debugging, so you may need Release mode or add --tailcalls+):

    let foo a cont = cont (a + 1)
    

    The function simply calls the function cont with the first argument incremented by one. In continuation passing style, you have a long sequence of such calls, so the optimization is crucial (you simply cannot use this style without some handling of tail calls). The generates IL code looks like this:

    IL_0000: ldarg.1
    IL_0001: ldarg.0
    IL_0002: ldc.i4.1
    IL_0003: add
    IL_0004: tail.                          // Here is the 'tail' opcode!
    IL_0006: callvirt instance !1 
      class [FSharp.Core] Microsoft.FSharp.Core.FSharpFunc`2<int32, !!a>::Invoke(!0)
    IL_000b: ret
    
    0 讨论(0)
提交回复
热议问题