JIT拒绝内联微小的方法

我缺少严肃的优化,因为JIT不会内联我的很多方法。

例如,让我们有以下代码:

static void Main(string[] args) { IsControl('\0'); } public static bool IsControl(char c) { return ((c >= 0 && c = 127 && c <= 159)); } 

在JIT编译之后生成以下内容:

 0000001f xor ecx,ecx 00000021 call FFFFFFFFFFEC9760 00000026 mov byte ptr [rsp+20h],al 0000002a nop 0000002b jmp 000000000000002D 0000002d add rsp,38h 00000031 rep ret 

请注意, 0000001f是我设置断点的地方。 如你所见,在00000021有一个电话,这是绝对错误的。 为什么这么小的方法不适合内联? 对于注释,这是通过优化编译的。

除了使用提前的源代码或字节码转换在指令到达JIT之前内联指令之外,没有办法要求JIT编译器内联您的方法。

如果您的算法对微优化非常敏感,那么删除调用指令会带来显着的性能优势,那么您可以考虑用不同的语言重写代码的性能关键部分,从而为控制该行为提供更广泛的工具。 根据您的问题的措辞,您似乎试图强迫C#进入一个设计为完全避免的问题空间。

使用MethodImplAttribute属性:

 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsControl(char c) { return ((c >= 0 && c <= 31) || (c >= 127 && c <= 159)); } 

看到

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.methodimplattribute.aspx

http://blogs.microsoft.co.il/sasha/2012/01/20/aggressive-inlining-in-the-clr-45-jit/

.Net的抖动具有内置的启发式function,可帮助它确定是否内联或不内联 。 因为我找不到一个阻止内联的好理由(见下文),并且在4.5中可以通过AggressiveInlining说服它,所以抖动可以内联,如果它想要的话,可能就是这样。 报价:

  1. 如果内联使代码变小,那么它所替换的调用就会很好。 请注意,我们讨论的是NATIVE代码大小,而不是IL代码大小(可能完全不同)。

  2. 特定呼叫站点的执行越多,它就越有利于inlning。 因此,循环中的代码应该比不在循环中的代码更内联。

  3. 如果内联暴露了重要的优化,那么内联是更理想的。 特别是具有值类型参数的方法因为这样的优化而受益于正常,因此具有内联这些方法的偏差是好的。

因此,给定内联候选者,X86 JIT编译器使用的启发式算法。

  1. 如果方法未内联,则估计调用站点的大小。

  2. 估计呼叫站点的大小(如果它是内联的(这是基于IL的估计,我们使用简单的状态机(马尔可夫模型),使用大量实际数据创建以形成此估计器逻辑)

  3. 计算乘数。 默认情况下为1

  4. 如果代码处于循环中,则增加乘数(当前启发式在循环中将其颠覆为5)

  5. 如果看起来结构优化会起作用,请增加乘数。

  6. 如果InlineSize <= NonInlineSize * Multiplier执行内联。


接下来是对我试图深入研究这一问题的描述,它可能会帮助处于类似情况的其他人。

我可以在.Net 4.5(x68和x64)上重现它,但我不知道它为什么没有内联,因为它没有内联显示停止器,如虚拟方法或消耗超过32个字节。 它短30个字节:

 .method public hidebysig static bool IsControl(char c) cil managed { // code size 30 (0x1e) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.0 IL_0002: blt.s IL_0009 IL_0004: ldarg.0 IL_0005: ldc.i4.s 31 IL_0007: ble.s IL_001c IL_0009: ldarg.0 IL_000a: ldc.i4.s 127 IL_000c: blt.s IL_001a IL_000e: ldarg.0 IL_000f: ldc.i4 0x9f IL_0014: cgt IL_0016: ldc.i4.0 IL_0017: ceq IL_0019: ret IL_001a: ldc.i4.0 IL_001b: ret IL_001c: ldc.i4.1 IL_001d: ret } // end of method Program::IsControl 

当启用AggressiveInlining (你说你不能,因为你在.Net 3.5上)时,不仅调用内联,而且内联代码完全被删除 – 因为你不应该使用返回值:

 --- Program.cs -------------------------------------------- IsControl('\0'); 00000000 ret 

NB我不确定你是否意识到除了使用Release构建模式之外,你还必须这样做

  • 转到Tools => Options => Debugging => General并确保未选中标记为“在模块加载时抑制JIT优化”的框。
  • 确保未选中标记为“启用我的代码”的复选框。

为了看JIT优化代码。 如果不这样做,您将得到以下内容,而不是上述单个ret语句:

 --- Program.cs -------------------------------------------- IsControl('\0'); 00000000 push rbp 00000001 sub rsp,30h 00000005 lea rbp,[rsp+30h] 0000000a mov qword ptr [rbp+10h],rcx 0000000e mov rax,7FF7F43335E0h 00000018 cmp dword ptr [rax],0 0000001b je 0000000000000022 0000001d call 000000005FAB06C4 00000022 xor ecx,ecx 00000024 call FFFFFFFFFFFFD3D0 00000029 and eax,0FFh 0000002e mov dword ptr [rbp-4],eax 00000031 nop } 00000032 nop 00000033 lea rsp,[rbp] 00000037 pop rbp 00000038 ret 

即使没有AggressiveInlining ,以下更短(而非等效)的方法btw也会被内联:

 public static bool IsControl(char c) { return c <= 31 || c >= 127; } 

当您进行微优化时,您的IsControl方法应该看起来像下列之一,具体取决于c值的(预期)实际分布:

 public static bool IsControl2(char c) { return c <= 31 || (c >= 127 && c <= 159); } public static bool IsControl3(char c) { return c <= 159 && (c <= 31 || c >= 127); } 

它将删除c >= 0 ( char的最小值为0 )的多余检查,在最坏的情况下将比较次数减少到3(尽管我没有检查抖动是否足够聪明以避免冗余检查并且它还将方法的代码大小从30字节减少到26字节,这可能会影响抖动决定是否内联。

Visual Studio 2017(及更早版本)调试器选项中有一个复选框,名为“ 抑制模块加载时的JIT优化(仅限管理) ”。 如果选中此选项,则无论Release还是Debug版本或[MethodImpl(MethodImplOptions.AggressiveInlining)]属性如何,在调试器中运行时都不会获得任何方法内联。

在此处输入图像描述