首先,我们给出一段可以优化的简单代码,然后再来看看CLR Jitter对其是如何进行运行时优化的。名为sample.cs的C#测试程序如下:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Sample starts."); //Console.WriteLine(String) jitted
//System.Diagnostics.Debugger.Break(); //give us a chance to attach debugger here
int i = 0;
i += 300; i += 300;
i += 300; i += 300;
i += 300; i += 300;
i += 300; i += 300;
i += 300; i += 300;
i += 300;
Console.WriteLine(i); //Console.WriteLine(Int32) jitted
}
}
1. 如何获取未经Jitter优化的汇编代码?
这一步非常简单,可以采用两种方法:
a. 使用vs2005,F7编译之后断点,然后通过Debug中的Disassembly窗口便可看到非jit优化的代码
b. 使用cordbg,首先使用/debug编译配置来手工编译(“csc /debug sample.cs”),然后启动cordbg,加载sample.exe(“run sample.exe”),然后再反汇编代码(“dis 20”),即可看到未经jit优化的汇编代码如下:
[ 0001 ] push eax
[ 0002 ] mov dword ptr [esp],ecx
[ 0005 ] cmp dword ptr ds:[00A72DDCh], 0
[000c] je 00000007
[000e] call 78DC8748
[ 0013 ] xor esi,esi
[ 0016 ] mov ecx,dword ptr ds:[023F2098h]
[001c] call dword ptr ds:[012F1364h]
[ 0023 ] xor esi,esi
[ 0025 ] add esi,12Ch // Non-optimization at all!
[002b] add esi,12Ch
[ 0031 ] add esi,12Ch
[ 0037 ] add esi,12Ch
[003d] add esi,12Ch
[ 0043 ] add esi,12Ch
[ 0049 ] add esi,12Ch
[004f] add esi,12Ch
[ 0055 ] add esi,12Ch
[005b] add esi,12Ch
[ 0061 ] add esi,12Ch
[ 0067 ] mov ecx,esi
[ 0069 ] call dword ptr ds:[012F1350h]
[ 0071 ] pop ecx
[ 0072 ] pop esi
[ 0073 ] ret
2. 如何查看Jit优化后的汇编代码?
查看jit优化之后的代码有点麻烦,其原因在于:对于所有调试器(无论是cordbg还是vs2005)启动的.net进程,jit优化功能是默认关闭的,而这里我们偏偏又非得使用调试器来查看其反编译的代码不可。
Microsoft推荐了一种基于cordbg的jit优化代码查看方式:通过将cordbg的模式参数JitOptimizations设为1(启动cordbg,在加载程序之前输入“mode JitOptimizations 1”),便可以要求jit输出优化之后的代码。但令人不解的是,我发现这种方式并不起作用,无论JitOptimizations参数为0还是为1,cordbg反汇编输出的都是未经jit优化的代码。(我的配置为.net framework 2.0.50727.42+vs2005+Sp1,网上有言论表明在.netfx 1.1中该参数是起作用的,但我没有来得及试)
我们可以采用另外一种办法来解决这个问题:在main()中插入一条System.Diagnostics.Debugger.Break()语句。在去掉sample.cs中Debugger.Break语句的注释之后,首先以/optimize+选项手工编译一遍(“csc /optimize+ sample.cs”,注意不要加/debug),然后直接运行sample.exe,当运行到break这一句时,会弹出一个对话框提示我们遇到了一个用户自定义断点是否需要调试,这里我们选择vs2005来调试 (注意不要选CLR Debugger,其功能不完善,无法step in/step out),进入vs2005之后,打开Disassembly窗口,在Address中输入“Program.Main”,即可查看到Jit优化之后的汇编代码如下:
[ 0007 ] jne [ 0013 ] //if 'Console.this' be equal to NULL?
[ 0009 ] mov ecx, 1
[000e] call 7859D79C //exception traps for safety or lazy construction
[ 0013 ] mov ecx,dword ptr ds:[023B1084h] //ecx: Console's this ptr
[ 0019 ] mov edx,dword ptr ds:[023B303Ch] //edx: ptr of the string object
[001f] mov eax,dword ptr [ecx] //eax: jmp-table of Console's methods
*[ 0021 ] call dword ptr [eax + [00D8h] // call writeline(String)
[ 0027 ] call 78642D70 //call break() directly - internal implementation
[002c] cmp dword ptr ds:[023B1084h], 0 // test whether Console be equal to NULL again
[ 0033 ] jne [003F]
[ 0035 ] mov ecx, 1
[003a] call 7859D79C
[003f] mov ecx,dword ptr ds:[023B1084h] //get Console's ptr
[ 0045 ] mov edx,0CE4h // Optimization result HERE! (const replacement)
[004a] mov eax,dword ptr [ecx] //get the jmp-table of Console's methods
*[004c] call dword ptr [eax + [00BCh] // call another overloaded ver. of writeline()
[ 0052 ] ret
可以看出,jit优化后的代码采用了一个编译时可计算的常量3300替代了i变量多次递增的冗余计算操作,从而一步到位,甚至连Main函数的栈框都省了不少。其中打了绿色星号的地方,分别是两次Console.WriteLine函数(两个重载版本)发生jit编译的地方;而这里的Debugger.Break()实际上是个InternalCall,即并不存在Break的IL代码实体,它实际上是一个CLR内部调用,因此无需jit,直接调用即可。
3. 在这个例子中,C#编译器有没有进行IL层面的优化?
在上面的分析过程中,我们只查看和比较了从IL转换至x86代码这个环节上,开、关Jit优化功能所产生的代码输出。不难发现,其实对于sample.cs中这种简单的情况而言,在使用csc对其进行编译时,即从C#源程序编译至IL代码时,即可进行相应的常数替换工作,而根本无需等到jit时再来进行优化。可事实上,csc究竟做了优化没有?不妨来验证一下,首先使用“csc /optimize+ sample.cs”编译一下,然后再用ildasm/reflector查看其main()的IL代码,令人感到意外的是,我们可以发现,虽然编译时使用了/optimize+选项,可得到的main()中的IL代码却是没有经过任何优化的(针对sample.cs中的这种场合)。这一方面说明,我们在上文中看到的代码的确是jit优化所为,而另一方面则说明,c#编译器在IL层面的优化能力实在很弱,弱到这么弱的sample都搞不定,这不禁让人怀疑csc究竟具不具备编译优化功能,或许/optimize仅仅只是一个运行时优化的标志?是不是microsoft认为,由于在jit层面提供的优化能力能够实现最大限度的跨编译器重用,因而没有必要在上层IL代码生成过程中考虑同样的优化问题呢?I don't know exactly. 无论真正的答案是什么,从本质而言,jit优化应该只是一种运行时的intra-procedual optimization,这便注定了过度依赖于其所产生的优化效果绝不会优于具备inter-procedual optimization功能的静态编译器。
值得指出的是,至少有一个编译器在IL层面的优化要远远优于csc.exe,那便是vs2005中vc++的编译器cl.exe,这一点我们可以通过将sample.cs改写成c++/cli方式,然后再通过ildasm查看其IL代码来确认,cl在编译时便完成了常量替换的优化工作。不仅如此,cl还提供了较全面的全局优化功能,如link-time优化和profile-guided优化等。因此,至少目前我们可以得出这样的结论:csc在IL层面的优化能力远不及cl,而c#程序运行的快慢在很大程度上取决于jit的优化效果;在多数纯托管环境中,c#程序的性能不大可能会超过充分全局优化之后的c++/cli程序。
4. Jit优化功能究竟在何时是开启了的?在何时它又是关闭的?
需要再次强调的是,所有的资料和实验都表明:一旦进程是由调试器(无论是vs2005、还是cordbg或者是CLR debugger)启动的,那么Jit优化功能便是默认关闭的。在vs2005中,即使采用的是release的编译方式,通过F5启动并运行程序时,Jit优化功能依然是关闭的。总之,这里的要点是:jit优化开关的初始状态更多地取决于程序的启动方式(是直接运行的?还是调试器启动的?),而不是编译选项!
这里我再提供一个简单的测试程序,大家可以通过运行它来得到一些直观的体会。
using System.Runtime.InteropServices;
class Program
{
[DllImport("kernel32.dll")]
public static extern bool QueryPerformanceCounter(ref ulong beepType);
static void Main(string[] args)
{
ulong x = 0, y = 0;
int i = 0;
QueryPerformanceCounter(ref x);
i += 300; i += 300;
i += 300; i += 300;
i += 300; i += 300;
i += 300; i += 300;
i += 300; i += 300;
i += 300;
QueryPerformanceCounter(ref y);
Console.WriteLine(y - x);
}
}
上述程序在我的机器上的大致测试结果如下:
a. Debug编译配置,调试器启动,输出为42
b. Release编译配置,调试器启动,输出为40 (老老实实地执行所有的递增指令)
c. Debug编译配置,直接启动,输出为6
d. Release编译配置,直接启动,输出为4 (所有与i相关的指令全部被优化掉了)
不难看出,无论采用何种配置编译,只要程序是直接启动的,那么jit优化功能便是开启的。Jit优化功能是否开启,与/debug、/optimize等编译选项无关。
5. 得到的结论
a. C#编译器在IL层面的优化力度不够,而VC++编译器则很强
b. 只要进程是由调试器启动的,jit优化功能默认关闭;如果是直接启动的,则jit优化默认开启
c. 如果c#程序是/debug编译的,用Debugger.Break方法attach vs2005之后,jit优化功能仍然是关闭的
d. 如果c#程序是/optimize编译的,那么用Break方法attach vs2005之后,jit优化却是开启的
e. .net framework 2.0 SDK中cordbg的JitOptimizations参数有问题,不能正常工作(待考)
f. 至今为止,除了IL编译时、jit运行时的两级优化之外,没有在CLR中发现任何“加载时优化”行为