方法是否在 JIT 编译期间使用 ?: 运算符内联?

Are Methods using the ?: Operator Inlined during JIT compilation?

我只是想知道在即时编译期间是否内联了一个使用 ?: 运算符的简单静态函数。这是使用代码的任意示例。

    public static int Max(int value, int max)
    {
        return value > max ? max : value;
    }

给出以下 IL:

Max:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldarg.1     
IL_0003:  bgt.s       IL_0008
IL_0005:  ldarg.0     
IL_0006:  br.s        IL_0009
IL_0008:  ldarg.1     
IL_0009:  stloc.0     
IL_000A:  br.s        IL_000C
IL_000C:  ldloc.0     
IL_000D:  ret 

或者更简单的选择是内联的吗?

        public static int Max(int value, int max)
        {
            if (value > max)
            {
                return max;
            }
            else
            {
                return value;
            }
        }

这是它的 IL:

Max:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldarg.1     
IL_0003:  cgt         
IL_0005:  stloc.0     
IL_0006:  ldloc.0     
IL_0007:  brfalse.s   IL_000E
IL_0009:  nop         
IL_000A:  ldarg.1     
IL_000B:  stloc.1     
IL_000C:  br.s        IL_0013
IL_000E:  nop         
IL_000F:  ldarg.0     
IL_0010:  stloc.1     
IL_0011:  br.s        IL_0013
IL_0013:  ldloc.1     
IL_0014:  ret

?: 运算符显然生成了比 if 替代方法更简洁的 MSIL,但是有人知道 JIT 编译期间发生了什么吗?两者都是内联的吗?它们中的任何一个都是内联的吗?

可以内联具有三元运算符的方法。 if/else 也是一种方法。当然这一切都取决于方法中的其他操作。

一种简单的检查方法是在方法中抛出异常并检查堆栈跟踪。如果方法是内联的,它将不会出现在堆栈跟踪中。

以下代码

class Program
{
    static void Main(string[] args)
    {
        try
        {
            int i = ThrowInTernaryOperator(1, 0);
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    public static int ThrowInTernaryOperator(int value, int max)
    {
        return value > max ? value / 0 : 0;
    }
}

在 .NET 4.6 64 位(发布版本)中抛出以下异常:

System.DivideByZeroException: Attempted to divide by zero.

at Test.Program.Main(String[] args)

堆栈跟踪中没有 ThrowInTernaryOperator,因此内联。

不同版本的 .NET 和 32/64 位架构的行为可能不同。

我们怎么知道的?让我们看看生成的代码。

这是一个测试程序:

internal static class Program
{
    public static int MaxA(int value, int max)
    {
        return value > max ? max : value;
    }

    public static int MaxB(int value, int max)
    {
        if (value > max)
            return max;
        else
            return value;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static int TestA(int a, int b)
    {
        return MaxA(a, b);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static int TestB(int a, int b)
    {
        return MaxB(a, b);
    }

    private static void Main()
    {
        var rand = new Random();
        var a = rand.Next();
        var b = rand.Next();

        var result = TestA(a, b);
        Console.WriteLine(result);

        result = TestB(a, b);
        Console.WriteLine(result);
    }
}

首先,让我们弄清楚一些事情。在 Release 构建中,MaxA 的 IL 是(在 Roslyn 上):

.method public hidebysig static 
    int32 MaxA (
        int32 'value',
        int32 max
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 8 (0x8)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: bgt.s IL_0006

    IL_0004: ldarg.0
    IL_0005: ret

    IL_0006: ldarg.1
    IL_0007: ret
} // end of method Program::MaxA

对于 MaxB,它是:

.method public hidebysig static 
    int32 MaxB (
        int32 'value',
        int32 max
    ) cil managed 
{
    // Method begins at RVA 0x2059
    // Code size 8 (0x8)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: ble.s IL_0006

    IL_0004: ldarg.1
    IL_0005: ret

    IL_0006: ldarg.0
    IL_0007: ret
} // end of method Program::MaxB

所以 IL 对于这两个函数是对称的(它是相同的代码,除了分支的顺序和分支指令是相反的)。

现在,让我们检查一下 TestATestB 的 x64 代码是什么样的。

TestA, x64, RyuJIT:

            return MaxA(a, b);
00007FFED5F94530  cmp         ecx,edx  
00007FFED5F94532  jg          00007FFED5F94538  
00007FFED5F94534  mov         eax,ecx  
00007FFED5F94536  jmp         00007FFED5F9453A  
00007FFED5F94538  mov         eax,edx  
00007FFED5F9453A  ret  

可以看到MaxA函数是内联的(没有call指令,可以清楚的看到jg"jump if greater"分支指令)。

TestB, x64:

            return MaxB(a, b);
00007FFED5F94550  cmp         ecx,edx  
00007FFED5F94552  jle         00007FFED5F94558  
00007FFED5F94554  mov         eax,edx  
00007FFED5F94556  jmp         00007FFED5F9455A  
00007FFED5F94558  mov         eax,ecx  
00007FFED5F9455A  ret  

毫不奇怪,我们得到了相同的结果。

为了增强竞争力,这里是 MaxA 在 x86 上:

            return MaxA(a, b);
00A32E22  in          al,dx  
00A32E23  cmp         ecx,edx  
00A32E25  jg          00A32E2B  
00A32E27  mov         eax,ecx  
00A32E29  jmp         00A32E2D  
00A32E2B  mov         eax,edx  
00A32E2D  pop         ebp  
00A32E2E  ret  

也内联。


作为参考,您可以使用 Disassembly window (Debug -> Windows -> Disassembly) 检查生成的汇编代码在断点上,但首先确保取消选中 在模块加载时抑制 JIT 优化 选项: