.NET编译技术内幕(2)
作者: builder.com
Tuesday, April 16 2002 12:23 PM
作为一种代码指令平台,Microsoft .NET比微软公司先前推出的其他技术平台要来得更为复杂。由于.NET提供了对多种编程语言以及(在理论上说)多重平台的支持,这就需要在传统的两个代码层添加一个中间代码层。在这里,传统的两层分别是源代码层和编译后的本机代码层。新加的代码层给.NET平台带来了额外的灵活性,不过,反过来却又增加了系统的复杂性。此外,由于这一新代码层的出现,一连串的新型应用程序部署选项也首次展现在了程序员的面前。这篇文章的主旨就是引领.NET应用程序的开发人员理解.NET的代码编译过程,了解附加的编译选项。
.NET之与众不同:MSIL
在Microsoft .NET框架内,应用程序可以用好多种高级程序语言编写、创建,例如VB.NET、C#乃至COBOL .NET等等都可以编写.NET应用程序。而通过每一种遵守.NET规范的编程语言所编写的程序代码首先都得通过一种初始编译步骤从源代码变成.NET的公共标准语言:MSIL(微软中介语言:Microsoft Intermediate Language)。MSIL自身是一种完整的、和对象相关的语言,只有它才可能创建出应用程序。为了大致了解MSIL的一些有关情况,你可以参看“通过MSIL语言了解CLR的运行原理”一文。.NET应用程序是以MSIL的形式出现的,只有在程序执行的时候才通过即时编译器(JIT)被编译为本机代码。图A就对这一过程进行了说明。
图A
.NET的编译过程:从源代码到本机指令
只要装载了assembly代码就会进行JIT编译,可见这是一种汇编级的编译(了解更多assembly技术的细节请参看“Assembly—治愈“DLL地狱”的良方?”一文)。在编译过程中,JIT编译器一旦首次遭遇对象的索引就会装载匹配对象各个方法声明的对应程序。这样,以后调用方法的时候就会编译其IL,而方法的对应根程序则被方法的编译后代码的地址所取代。这一过程在每次方法被首次调用的时候进行,产生的本机代码则被缓冲以便会话过程中下次装载assembly代码的时候可以被使用。显然,这样的指令系统相比传统的编译语言需要更大的处理能力,不过其要求也没有你想象的那么高。
在这里必须澄清一个普遍误解的错误概念,那就是不少人认为.NET应用程序是解释型而非编译型的程序。另外,还有这样的常见错误认识:JIT编译的代码存储在磁盘上并且可以为同一应用程序执行。虽然这样做也不是不可以,但是,你很快就会明白,这可不是缺省的编译方案。应用程序的IL代码实际上在每次应用程序运行的时候都会被重新编译为本机代码。
两种编译器
事实上,JIT编译器分成两种(经济编译器和普通编译器),而且它们生来也不是平等的。经济JIT编译器代表了运行一个.NET应用程序所需要的最少功能,它直接用对等的本机代码取代每一条MSIL指令,不进行任何优化从而也带来更少的系统负载。这也意味着它主要应用在内存等资源比较紧张的平台上。
另一方面,普通JIT编译器则是缺省的运行时配置,它会对其产生的代码进行即时优化。这样做无形中给予了.NET超出传统预编译语言的一个优点:预编译语言只能对其处理的代码将要运行于其上的平台做一番大致的事前估计。JIT编译器可以经过准确调节达到当前运行时状态,结果可以完成一些预编译语言无法完成的工作:
更高效地利用和分配CPU寄存器
在适当的情况下实施低级代码优化,比如常量重叠、拷贝复制、取消范围检查、取消常规副表达式以及方法内联等
在代码执行期间监控当前的物理和虚拟内存需求从而更高效地利用内存
产生特定的平台指令以准确、充分地利用实际的处理器模式
.NET编译的结果就是JIT所带来的额外负载要求并没有产生显著的性能损失。
性能选项
这就是说,每次运行应用程序时MSIL就会被JIT编译。记住,这就是常识了,然后,根据以上内容中说明的原理,在开始启动应用程序以及首次使用非核心功能的时候显然会导致低于优化级的系统性能表现。那么你又该采取什么措施把这种负面影响降低到最小呢?
微软公司的对策是为我们提供了一种名为Pre-JIT的编译器(也被称做本机映像生成器:Native Image Generator,程序名因此是Ngen.exe)。从表面上看,至少它也算是应付任何性能问题的一项治疗手段。Pre-JIT编译器在运行时之前被调用,在安装时,它会把全部assembly形式的MSIL编译为本机代码。这种本机代码随后存储在全局assembly缓存(Global Assembly Cache)的某一个特殊部分供以后使用,这样就完全绕过了JIT编译过程。
乍看之下,这样做应该是解决先前的问题了,对客户端代码而言尤其如此。但是,你还记得吗?普通JIT在编译MSIL的时候实施了大量的即时优化操作。而许多此类的优化操作,尤其是那些牵扯到寄存器和存储器使用的优化,都是由系统的当前需求所驱动的。所以,批量编译assembly代码的举措就会阻止这些优化的进行从而在实际上产生出运行更慢的最终代码。在你采用这个法子之前,微软的建议是,比照普通编译下的当前条件,把你的JIT和Ngen版本设置为目标平台上的同一汇编级。
除非你是Java的铁杆拥趸,.NET的运行时行为和编译器都将同你以前曾经了解的同类情况完全不同。但是它们也不是什么神秘的东西。我希望我的阐述能澄清一些曾经令你感到迷惑的问题。