有没有一种方法可以查看JIT编译器为给定的C# / CIL代码生成的本机代码?

20

这个回答中,有人建议使用位移操作符来代替整数乘除法以提高性能。我在评论中质疑这是否真的会更快。在我的脑海中,有一个想法,即在某个级别上,某些智能的编译器将会很聪明地认为>>1 /2是相同的操作。但现在我怀疑这是否属实,如果是,它发生在哪个级别上。

一个测试程序生成了两个方法的比较CIL代码(使用optimize选项)分别进行除法和位移操作:

  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.2
  IL_0002:  div
  IL_0003:  ret
} // end of method Program::Divider

对比

  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.1
  IL_0002:  shr
  IL_0003:  ret
} // end of method Program::Shifter
因此,C#编译器正在发出divshr指令,而没有聪明地进行优化。我现在想看看JITter生成的实际x86汇编代码,但我不知道该如何做到这一点。这是否可能?
编辑添加
发现结果
谢谢大家的答案,我接受了nobugz的答案,因为它包含了关于调试选项的关键信息。最终对我有用的是:
1. 切换到“发布”配置 2. 在“工具|选项|调试器”中,关闭“抑制模块加载时的JIT优化”(即我们希望允许JIT优化) 3. 同上,在“启用我的代码”处关闭“启用Just My Code”(即我们要调试所有代码) 4. 在某个地方放置一个Debugger.Break()语句 5. 构建程序集 6. 运行.exe文件,当它停止时,使用现有的VS实例进行调试 7. 现在Disassembly窗口会显示实际要执行的x86汇编代码
结果非常启发人 - 原来JITter实际上可以执行算术运算!以下是Disassembly窗口中编辑过的样本。各种-Shifter方法使用>>进行二的幂次数除法;各种-Divider方法使用/进行整数除法。
 Console.WriteLine(string.Format("
     {0} 
     shift-divided by 2: {1} 
     divide-divided by 2: {2}", 
     60, TwoShifter(60), TwoDivider(60)));

00000026  mov         dword ptr [edx+4],3Ch 
...
0000003b  mov         dword ptr [edx+4],1Eh 
...
00000057  mov         dword ptr [esi+4],1Eh 

两种静态除以2的方法不仅已经被内联,而且实际计算已由JITter完成。

Console.WriteLine(string.Format("
    {0} 
    divide-divided by 3: {1}", 
    60, ThreeDivider(60)));

00000085  mov         dword ptr [esi+4],3Ch 
...
000000a0  mov         dword ptr [esi+4],14h 

同静态除以3的情况相同。

Console.WriteLine(string.Format("
    {0} 
    shift-divided by 4: {1} 
    divide-divided by 4 {2}", 
    60, FourShifter(60), FourDivider(60)));

000000ce  mov         dword ptr [esi+4],3Ch 
...
000000e3  mov         dword ptr [edx+4],0Fh 
...
000000ff  mov         dword ptr [esi+4],0Fh 

并且进行静态除以4。

最好的:

Console.WriteLine(string.Format("
    {0} 
    n-divided by 2: {1} 
    n-divided by 3: {2} 
    n-divided by 4: {3}", 
    60, Divider(60, 2), Divider(60, 3), Divider(60, 4)));

0000013e  mov         dword ptr [esi+4],3Ch 
...
0000015b  mov         dword ptr [esi+4],1Eh 
...
0000017b  mov         dword ptr [esi+4],14h 
...
0000019b  mov         dword ptr [edi+4],0Fh 

这是内联的,然后计算所有这些静态除法!

但如果结果不是静态的呢?我添加了从控制台读取整数的代码。对于这些除法的计算结果如下:

Console.WriteLine(string.Format("
    {0} 
    shift-divided by 2:  {1} 
    divide-divided by 2: {2}", 
    i, TwoShifter(i), TwoDivider(i)));

00000211  sar         eax,1 
...
00000230  sar         eax,1 

因此,尽管CIL不同,JITter知道除以2等同于右移1位。

Console.WriteLine(string.Format("
    {0} 
    divide-divided by 3: {1}", i, ThreeDivider(i)));

00000283 idiv eax,ecx

它知道需要除以3。

Console.WriteLine(string.Format("
    {0} 
    shift-divided by 4: {1} 
    divide-divided by 4 {2}", 
    i, FourShifter(i), FourDivider(i)));

000002c5  sar         eax,2 
...
000002ec  sar         eax,2 

它知道将一个数除以4等价于将其右移两位。

最后(再次是最好的!)

Console.WriteLine(string.Format("
    {0} 
    n-divided by 2: {1} 
    n-divided by 3: {2} 
    n-divided by 4: {3}", 
    i, Divider(i, 2), Divider(i, 3), Divider(i, 4)));

00000345  sar         eax,1 
...
00000370  idiv        eax,ecx 
...
00000395  sar         esi,2 

它已经内联了方法,并根据静态可用参数找到了最佳执行方式。很好。


因此,在 C# 和 x86 之间的堆栈中,确实有些东西足够聪明,能够发现 >> 1/ 2 是相同的。所有这些都让我更加坚信,将 C# 编译器、JITter 和 CLR 结合起来比我们作为卑微的应用程序员尝试的任何小技巧都要聪明得多 :)


你能分享一下你的发现,造福我们所有人吗?谢谢 :) - flesh
3个回答

8

除非您配置调试器,否则您将无法获得有意义的结果。转到“工具”+“选项”,选择“调试”,然后选择“通用”,关闭“在模块加载时禁止JIT优化”。切换到发行版配置。示例代码如下:

static void Main(string[] args) {
  int value = 4;
  int result = divideby2(value);
}

如果拆卸出来的东西看起来像这样,你就做得对了:
00000000  ret  

您需要欺骗JIT优化器以强制表达式被评估。使用Console.WriteLine(variable)可以帮助。然后您应该会看到类似于这样的内容:

0000000a  mov         edx,2 
0000000f  mov         eax,dword ptr [ecx] 
00000011  call        dword ptr [eax+000000BCh] 

没错,它在编译时评估了结果。 这很有效,不是吗。


为什么在“模块加载时抑制JIT优化”开启时,你无法获得有意义的结果?我的理解是,启用此选项只意味着你正在调试经过优化的/“发布”本机代码。 - Justin

3
是的。Visual Studio内置了反汇编器来执行此操作。不过您需要将该命令添加到菜单栏中。请前往“附加组件/自定义/命令”(我不知道英文版本是否真的叫这个名字),并将“反汇编”命令添加到您的菜单栏中,该命令位于调试下面的某个位置。
然后,在程序中设置一个断点,当它触发时,点击此“反汇编”命令。Visual Studio会显示您的机器码的反汇编结果。
以下是Divider方法的示例输出:
public static int Divider(int intArg)
    {
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  
00000006  sub         esp,34h 
00000009  mov         esi,ecx 
0000000b  lea         edi,[ebp-38h] 
0000000e  mov         ecx,0Bh 
00000013  xor         eax,eax 
00000015  rep stos    dword ptr es:[edi] 
00000017  mov         ecx,esi 
00000019  xor         eax,eax 
0000001b  mov         dword ptr [ebp-1Ch],eax 
0000001e  mov         dword ptr [ebp-3Ch],ecx 
00000021  cmp         dword ptr ds:[00469240h],0 
00000028  je          0000002F 
0000002a  call        6BA09D91 
0000002f  xor         edx,edx 
00000031  mov         dword ptr [ebp-40h],edx 
00000034  nop              
    return intArg / 2;
00000035  mov         eax,dword ptr [ebp-3Ch] 
00000038  sar         eax,1 
0000003a  jns         0000003F 
0000003c  adc         eax,0 
0000003f  mov         dword ptr [ebp-40h],eax 
00000042  nop              
00000043  jmp         00000045 
    }

2
不需要将该命令添加到VS中,它在“调试->窗口”菜单上。 - Lasse V. Karlsen

2

当您进行调试时(仅在调试时),只需单击“调试” - “窗口” - “反汇编”或按相应的快捷键Ctrl+Alt+D。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接