JIT的工作流程

JIT(Just in time)即时编译器:在执行时负责把编译生成的IL代码转换成本机代码(CPU指令)。对于以下代码来说:

void Main()
{
	Console.WriteLine("hello");
	Console.WriteLine("word");
}

当执行Main方法时,CLR会检测Main所使用到的类型,并分配一个数据结构用来存储对这些类型的访问。这段代码里只涉及到了Console类,所以Console类的所有方法都被存到了那个数据结构里,每个方法都是一个记录项,每个记录项都有一个地址,可根据这个地址找到方法的实现。每个记录项指向一个被称为JITCompiler的CLR函数。

首次调用到Console的WriteLine方法时,JITCompiler函数就会被调用,并将IL代码编译成CPU指令。具体JITCompiler函数干了以下这几件事:

  1. 在Console类中,查找WriteLine这个方法。
  2. 从元数据中获取这个方法的IL代码。
  3. 分配内存块
  4. 将IL代码编译成本机代码,并将这些指令存储到上一步分配的内存里。
  5. 修改记录项,将指向从JITCompiler改为内存块地址。
  6. 跳转到内存块中的本机代码。

再次调用Console的WriteLine方法时,发现内存块里已经有WriteLine方法的本机代码了,所以就跳过JITCompiler函数,直接执行内存里的本机代码。

所以对同一个方法来说,只有第一次执行时会需要JIT编译,所以比较慢。

本机代码生成器:NGen.exe

正是JIT有第一次执行慢和额外耗费一点内存的问题,所以就有了NGen,当软件安装到用户的计算机上时,NGen就可以将IL代码编译成本机代码(JIT是在运行时才编译)。所以即使是第一次运行,速度也很快。而且没有IL代码,也不存在代码泄露问题。但这些只是看起来很美好,它还有如下几个问题:

  1. 许多人以为只要发布由NGen生成的文件而不发布哪些IL代码的文件就能防止代码泄露。但这是不可能的,在运行的时候CLR要求访问程序集的元数据(用户反射和序列化等功能),所以你还得把IL代码发上去。
  2. CLR因为某些原因没法使用NGen生成的这些文件,此时就需要JIT和IL代码文件。具体有以下几个原因:
    • CLR版本不一样,CPU类型不一样,Windows操作系统版本不一样。
    • 程序集的ID不一样(重新编译会引起改变),引用的程序集的ID不一样,安全性发生了改变(如吊销了之前授予的权限)
  3. 可能有较差的执行性能:NGen不能优化使用特定的CPU指令。静态字段的实际地址因为只能在运行时确定,所以只能间接访问静态字段不能直接访问,等等。测试表明,在某些情况下NGen编译的程序相对于JIT来说反而会慢5%左右。

正式如此,对于NGen的使用需要格外谨慎,而且必须经过严格的测试。对于服务端程序来说NGen的作用并不明显,因为只有第一个客户请求时会感到性能下降,后续请求都是直接从内存里拿本机代码执行,所以执行很快。对于客户端程序来说NGen也许能提高启动速度。

JIT和NGen的比较

  1. JIT只有在第一次执行时需要进行编译,所以会比较慢。NGen第一次也会很快。
  2. 但是NGen因为没法利用特定的CPU指令进行优化,所以总体运行效率不一定比JIT快。
  3. 因为反射和序列化需要元数据,而且在某些情况下CLR没法使用NGen生成的本机代码,所以还需要把IL代码和元数据发布上去。所以NGen也照样没法保护你的源代码。