IL比大多数CPU机器语言要高级的多,IL能访问和操作对象类型,并提供相应的指令来创建和初始化对象,在对象上调用虚方法,并能直接操作数组元素,甚至提供了用于抛出异常和捕获异常的指令,以实现错误处理。因此IL可以被看作是一种面向对象的机器语言。
开发人员使用C#、VB、C++等高级语言来编程实现业务逻辑,这些高级语言的编译器会将源代码编译为IL。
为了执行具体的某一个方法,这个方法对应的IL首先必须转换为本地CPU指令。这是CLR的JIT(Just-in-time)即时编译器的职责。
上面这段代码是如何执行的?
在Main方法执行之前,CLR会检测出Main的代码引用的所有类型,CLR会分配一个内部数据结构,用于管理对引用类型的访问。
示例代码中,Main方法引用了单一类型Console,CLR为Console分配了一个单独的内部数据结构,Console类型中每个方法都对应一条记录,每条记录都容纳了一个地址,根据这个地址就可以找到方法的实现。CLR对这个内部数据结构初始化的时候,每条记录都设置成CLR内部包含的一个未文档化的函数,这个函数称为JITCompiler。
Main方法首次调用WriteLine时,会调用JITCompiler函数,JITCompiler函数负责将这个方法的IL代码编译为本地CPU指令,由于IL是“即时”编译的,所以通常将CLR这个即时编译组件称为JIT编译器。
第一次调用Console.WriteLine这个方法时:
1. JITCompiler函数被调用时,它知道要调用哪个方法,以及具体的类型定义了该方法。
2. 然后,JITCompiler会在程序集的元数据中搜索被调用方法的IL。
3. JITCompiler验证IL代码,并将IL代码编译成本地CPU指令,同时本地CPU指令保存在一个动态分配的内存中。
4. 然后,JITCompiler回到CLR为类型创建的内部数据结构中,找到与被调用方法对应的那条记录,将最初调用它的那个引用替换成CPU指令内存块的地址。
5. 最后,JITCompiler函数会跳转到内存块中的代码(CPU指令),即Console.WriteLine方法的具体实现。
6. 代码执行完毕后,返回到Main方法中,继续执行其他代码。
Main函数第二次调用Console.WriteLine方法
第二次调用时,由于已经对WriteLine方法进行了验证和编译,所以会直接执行内存块中代码(CPU指令),完全跳过了JITCompiler函数。因此,一个方法只有在第一次调用时才会造成一定的性能消耗。后续对此方法的调用都以本地代码的方法全速运行。
总结:
1. 一个方法只有在第一次调用时的JIT编译才会造成一定的性能消耗。后续对此方法的调用都以本地代码的方法全速运行
2. JIT编译器将本地CPU指令保存在动态内存中,一旦应用程序终止,编译好的本地CPU指令代码会被丢弃。所以,如果关闭重新运行应用程序,或者同时启动了应用程序的两个实例(两个不同的进程),JIT编译器必须再次将IL编译成本地CPU指令.
3. 对于大多数应用程序来说,因JIT编译造成的性能损失并不明显,大多数应用程序倾向于反复调用相同的方法。
4. 同时,CLR的JIT编译器会对本地代码进行优化。代码在优化后将获得更出色的性能。
先临时写到这,10年后重读CLR Via C#,更多的收获是,对.Net 底层技术原理的理解更深,同时有了更敬畏之心。后续计划再重读更多内容,分享给大家。
知识在与分享和总结!