.Net 7 CLR和ILC编译函数过程

楔子

由于甲方的需求,随着研究深入,发现CLR编译函数与ILC编译是两种不同的截然方式,除了JIT部分编译一样,其它部分貌似完全不一。

本篇来梳理这些东西

示例:

作为例子,先上一段非常简单的代码:.

    internal class Program
    {
        static void Main(string[] args)
        {
            A();
        }

        static void A()
        {
            B();
        }
        static void B()
        {
        }
    }

CLR编译

上面的例子,如果是CLR来编译,假设从Main函数入口开始,它首先是是通过CLR加载Main函数的IL代码来调用JIT,构建一个汇编层面代码。然后跳转到Main函数的起始位置,也就是函数头处执行。当执行的过程中,遇到了Main函数里面调用了A函数。它会识别A函数的IL代码,调用JIT把IL代码编译成机器码,然后跳转到A函数的函数头,在A函数执行的过程中又遇到B函数,它识别B函数的IL代码,然后调用JIT把IL代码编译成机器码。跳转到B函数的函数头汇编位置执行。执行完毕,然后又跳转到刚刚调用B函数的A函数的下一条指令开始执行,执行完毕。跳转到调用A函数的Main函数里面的下一条指令开始执行。Main执行完成之后,整个运行过程执行完毕。

这上面一大段的话语,是自己理解而成。画成图片就如下所示:
.Net 7 CLR和ILC编译函数过程

ILC编译

1.表位:
ILC的编译迥异于CLR的编译,它主要是通过重定位向量表IMAGE_BASE_RELOCATION来构建一个编译过程。

上面的CLR因为是通过递归来查找当前需要编译的函数,这个过程看似没问题,但是实际上当函数第一次运行的时候,就需要调用JIT。比如某一个函数运行了很多次,但是某次调用了某个特殊函数,这个特殊函数刚好第一次运行(其它时候都没调用,可能if else这种语句),恰巧如果这个函数编译时间较长,这样就拖累整体的运行性能。如果这种情况运行几次,或者十几次,或者更多,那么导致整个程序性能拉胯不堪。

2.解决
为了解决这个问题,微软团队搞了一个天才的设想。就是把所有的函数,事先编译一遍,等到运行的时候,直接调用这个编译之后的结果就行了。刚好社区有AOT编译需求,于是这个功能就用在了AOT上。

但是AOT的短处也是显而易见的,最常见的缺陷就是它的优化性能不如JIT。为了解决这个问题,于是R2R技术又天才般的冒出来了。它既解决了AOT优化问题,又解决了JIT预热问题。
所以就有了ILC与R2R的Crossgen2共享代码的现状。

3.过程

这里只是看看,ILC编译过程,其余不论。ILC是把所有函数全部事先编译一遍,然后写入到.O OR .Obj里面,最后链接到二进制可执行文件。了解了这个原理,再来看它编译过程。

如何知道一个函数它所依赖的所有函数呢?比如例子里面,你调用Main函数,Main函数里面又调用了A函数,A函数里面又调用了B函数。
如何通过Main函数知道A函数和B函数的存在,然后把Main,A,B三个函数进行事先编译呢?

要解决这个问题,需要JIT里面的重定位向量表。ILC在编译到时候,把所有需要用到的引用进行JIT编译。
当JIT编译一个函数的时候,它会在这个函数的汇编代码层面标记一些东西。比如它编译Main函数的时候,在Main函数里发现里面调用了A函数,假设上面例子编译的托管DLL是ConsoleApp1.Dll。那么A函数就会被注释成:ConsoleApp1.Program.A这种形式,然后把它放到基址重定位向量表里。
此后,它会循环被编译的函数和基址重定位向量表。把编译的函数添加到全局栈,如果发现函数包含基址向量表,就会把这个向量表进行子循环,把每个向量表里的函数添加到全局站。然后查找向量表里面的函数是否包含函数,如果有,则继续注释,放到向量表,循环。

4.图示
.Net 7 CLR和ILC编译函数过程

结尾

以上都是通过Debug代码理解而来,比较晦涩。
纯粹是甲方的某些需求需要用到。