有没有办法查看 JITter 为给定的 C# / CIL 生成的本机代码?
测试程序为分别除法和移动其参数的两种方法生成以下比较 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# 编译器发出 div
或 shr
指令,但并不聪明。我现在想查看 JITter 生成的实际 x86 汇编程序,但我不知道如何执行此操作。有可能吗?
编辑添加
发现
感谢您的回答,已接受 nobugz 的回答,因为它包含有关该调试器选项的关键信息。最终对我有用的是:
- 切换到发布配置
- 在
Tools | 中 选项|调试器
,关闭“抑制模块加载时的 JIT 优化”(即我们想要允许 JIT 优化) - 同一位置,关闭“仅启用我的代码”(即我们想要调试< em>所有代码)
- 在某处放置一个
Debugger.Break()
语句 - 构建程序
- 集运行.exe,当它中断时,使用现有的VS实例进行调试
- 现在反汇编窗口向您显示将要执行的实际 x86
至少可以说,结果很有启发性 - 事实证明 JITter 实际上可以进行算术运算!以下是“反汇编”窗口中经过编辑的示例。各种 -Shifter
方法使用 >>
除以 2 的幂;各种 -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
完成
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
两种 statically-divide-by-2 方法不仅已内联,而且实际计算已由JITter 静态除以 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 就是右移 2。
最后(又是最好的!)
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 添加到一起会比我们可以尝试的任何小技巧更加聪明。应用程序程序员:)
In a comment on this answer (which suggests using bit-shift operators over integer multiplication / division, for performance), I queried whether this would actually be faster. In the back of my mind is an idea that at some level, something will be clever enough to work out that >> 1
and / 2
are the same operation. However, I'm now wondering if this is in fact true, and if it is, at what level it occurs.
A test program produces the following comparative CIL (with optimize
on) for two methods that respectively divide and shift their argument:
IL_0000: ldarg.0
IL_0001: ldc.i4.2
IL_0002: div
IL_0003: ret
} // end of method Program::Divider
versus
IL_0000: ldarg.0
IL_0001: ldc.i4.1
IL_0002: shr
IL_0003: ret
} // end of method Program::Shifter
So the C# compiler is emitting div
or shr
instructions, without being clever. I would now like to see the actual x86 assembler that the JITter produces, but I have no idea how to do this. Is it even possible?
edit to add
Findings
Thanks for answers, have accepted the one from nobugz because it contained the key information about that debugger option. What eventually worked for me is:
- Switch to Release configuration
- In
Tools | Options | Debugger
, switch off 'Suppress JIT optimization on module load' (ie we want to allow JIT optimization) - Same place, switch off 'Enable Just My Code' (ie we want to debug all code)
- Put a
Debugger.Break()
statement somewhere - Build the assembly
- Run the .exe, and when it breaks, debug using the existing VS instance
- Now the Disassembly window shows you the actual x86 that's going to be executed
The results were enlightening to say the least - it turns out the JITter can actually do arithmetic! Here's edited samples from the Disassembly window. The various -Shifter
methods divide by powers of two using >>
; the various -Divider
methods divide by integers using /
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
Both statically-divide-by-2 methods have not only been inlined, but the actual computations have been done by the 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
Same with statically-divide-by-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
And statically-divide-by-4.
The best:
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
It's inlined and then computed all these static divisions!
But what if the result isn't static? I added to code to read an integer from the Console. This is what it produces for the divisions on that:
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
So despite the CIL being different, the JITter knows that dividing by 2 is right-shifting by 1.
Console.WriteLine(string.Format("
{0}
divide-divided by 3: {1}", i, ThreeDivider(i)));
00000283 idiv eax,ecx
And it knows you have to divide to divide by 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
And it knows that dividing by 4 is right-shifting by 2.
Finally (the best again!)
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
It has inlined the method and worked out the best way to do things, based on the statically-available arguments. Nice.
So yes, somewhere in the stack between C# and x86, something is clever enough to work out that >> 1
and / 2
are the same. And all this has given even more weight in my mind to my opinion that adding together the C# compiler, the JITter, and the CLR makes a whole lot more clever than any little tricks we can try as humble applications programmers :)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
在配置调试器之前,您不会获得有意义的结果。工具+选项、调试、常规,关闭“在模块加载时抑制 JIT 优化”。切换到发布模式配置。示例片段:
如果反汇编如下所示,那么您就做得对:
您必须欺骗 JIT 优化器以强制对表达式求值。使用 Console.WriteLine(variable) 会有帮助。然后您应该看到类似这样的内容:
是的,它在编译时评估了结果。效果很好,不是吗。
You won't get meaningful results until you configure the debugger. Tools + Options, Debugging, General, turn off "Suppress JIT optimization on module load". Switch to the Release mode configuration. A sample snippet:
You are doing it right if the disassembly looks like this:
You'll have to fool the JIT optimizer to force the expression to be evaluated. Using Console.WriteLine(variable) can help. Then you ought to see something like this:
Yup, it evaluated the result at compile time. Works pretty well, doesn't it.
是的。 Visual Studio 有一个内置的反汇编器来执行此操作。不过,您必须将该命令添加到菜单栏。转到 Extras/Customize/Commands(不过我不知道它们在英文版本中是否真的是这样调用的)并将命令 Dissassemble(位于“调试”下)添加到菜单栏的某处。
然后,在程序中设置一个断点,当断点中断时,单击此反汇编命令。 VS 将显示反汇编的机器代码。
除法器方法的输出示例:
Yes. Visual Studio has a built in disassembler to do that. You have to add the command to your menu bar though. Go to Extras/Customize/Commands (I don't know if they are really called that way in the english version though) and add the command Dissassembly, which is unter Debugging, somewhere to your menu bar.
Then, set a breakpoint in your program and when it breaks, click on this Disassembly command. VS will show you the disassembled machine code.
Example output for a Divider-method:
在调试时(并且仅在调试时)只需单击“调试”-“Windows”-“反汇编”或按相应的快捷键 Ctrl+Alt+D。
While you're debugging (and only while you're debugging) just click at Debug - Windows - Disassembly or press the corresponding shortcut Ctrl+Alt+D.