方法是否在 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 对于这两个函数是对称的(它是相同的代码,除了分支的顺序和分支指令是相反的)。
现在,让我们检查一下 TestA
和 TestB
的 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 优化 选项:
我只是想知道在即时编译期间是否内联了一个使用 ?: 运算符的简单静态函数。这是使用代码的任意示例。
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 对于这两个函数是对称的(它是相同的代码,除了分支的顺序和分支指令是相反的)。
现在,让我们检查一下 TestA
和 TestB
的 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 优化 选项: