zoukankan      html  css  js  c++  java
  • CLR系列:浅析.NET的JIT编译

    JIT(Just In Time,这是我们通过.net编译器生成的应用程序最终面向机器的编译器,因此大家对JIT了解一下其工作原理还是很有必要的。最近研究了一下,参考了很多文章,也在msdn上查了些资料,以下是最近我对JIT的理解和总结。大家都知道CLR只执行本机的机器代码。目前有两种方式产生本机的机器代码:实时编译(JIT)和预编译方式(产生native image)。下面,我想分析一下JIT。当我们需要校调对性能要求很高的代码时,查看IL通常不是最好的做法,因为JIT优化器会默默的优化我们的代码,使用reflector或者ildasm你能很快发现releasedebug模式下产生的IL代码几乎完全相同,那么是什么让release模式的代码运行起来如此迅速呢?这就是JIT优化的结果,通过查看managed代码(IL代码),我们没有办法看到这些优化,所以我们将通过native code(本地代码)来查找系统所做的优化,CLR使用类型的方法表来路由所有的方法调用。只有当要调用某个方法时,JIT编译器才会将IL的方法体编译为相应的本机机器码版本。这样可以优化程序的工作集。

    首先我们需要一段测试代码:

     

     1 static int i;
     2 static void Main( string[] args )
     3 {
     4     i = And( i, 0 );
     5     //PrintLine();
     6     Console.Read();
     7 }
     8 static int And( int i1, int i2 )
     9 {
    10     return i1 & i2;
    11 }
    12 static void Print()
    13 {
    14      PrintLine(); 
    15 }
    16 static void PrintLine()
    17 {
    18     Console.WriteLine( "GuoJun" );
    19 }

    下面通过Windbg+sos调试看看debug下JIT为我们的代码产生的native code(本地代码):

    0:003> .load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\SOS.dll   加载SOS模块

    0:003> !name2ee JIT.exe JIT.Program   查看Program类的信息。

    Module: 00a82c3c (JIT.exe)
    Token: 
    0x02000003
    MethodTable: 00a83060
    EEClass: 00a81214
    Name: JIT.Program

    这个命令给出了有关我们的类的丰富信息,上面有许多有用的信息,但是其中最重要的莫过于方法描述(MethodTable)的地址,我们可以用这个地址找到更多信息。

    0:003> !dumpmt -md 00a83060

    MethodDesc Table
       Entry MethodDesc      JIT Name
    79371278   7914b928   PreJIT System.Object.ToString()
    7936b3b0   7914b930   PreJIT System.Object.Equals(System.Object)
    7936b3d0   7914b948   PreJIT System.Object.GetHashCode()
    793624d0   7914b950   PreJIT System.Object.Finalize()
    00db0070   00a83038      JIT JIT.Program.Main(System.String[])
    00db00b8   00a83040      JIT JIT.Program.And(Int32, Int32)
    00a8c019   00a83048     NONE JIT.Program.Print()
    00a8c01d   00a83050     NONE JIT.Program.PrintLine()
    00a8c021   00a83058     NONE JIT.Program..ctor()

     在这里很多人已经注意到我们的方法还没有被JIT编译,这就是为什么我们通过间接引用来调用方法的原因之一 :main方法编译他之前,程序并不知道需要到哪里去调用.这就引出了一个问题:

    JIT怎么知道何时编译一个方法

    本质上来说,JIT是延迟加载我们的模块,通过一种被叫做块的技术,JIT能捕获到我们对方法的第一次调用,所谓块是一小段非托管代码,当我们第一次加载某个类型的时候,CLR通过Emit生成块.块简单的包含对JIT的调用。

    当一个方法第一次被调用的时候,调用方从MethodTable中读取指向一个代码块的地址,也就是方法的描述MethodDesc),然后调用这个块,块接着调用JIT。关键的地方在于,JIT完成了编译后,将改变MethodTable,使其直接指向已经被JIT编译过的代码,也就是说无论代码是否被JIT编译,对方法的调用都是通过调用MethodTable中方法地址来实现的,若代码尚未编译,则这个地址指向一个代码块,他会帮助你编译代码,然后修改MethodTable中的指针,指向本地代码。

    下面我们看看debug下Main方法的本地代码:

    0:003> !u 00db0070
    Normal JIT generated code
    JIT.Program.Main(System.String[])
    Begin 00db0070, size 
    34
    >>> 00db0070 56              push    esi
    00db0071 
    50              push    eax
    00db0072 890c24          mov     dword ptr [esp],ecx
    00db0075 833d082ea80000  cmp     dword ptr ds:[0A82E08h],
    0
    00db007c 
    7405            je      00db0083
    00db007e e8c4823779      call    mscorwks
    !CorLaunchApplication+0x972d (7a128347) (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
    00db0083 
    90              nop
    00db0084 8b0d1830a800    mov     ecx,dword ptr ds:[0A83018h]
    00db008a 33d2            xor     edx,edx
    00db008c ff159c30a800    call    dword ptr ds:[0A8309Ch] (JIT.Program.And(Int32, Int32), mdToken: 
    06000008)
    00db0092 8bf0            mov     esi,eax
    00db0094 89351830a800    mov     dword ptr ds:[0A83018h],esi
    00db009a e82d996378      call    mscorlib_ni
    +0x3299cc (793e99cc) (System.Console.Read(), mdToken: 060007b6)
    00db009f 
    90              nop
    00db00a0 
    90              nop
    00db00a1 
    59              pop     ecx
    00db00a2 5e              pop     esi
    00db00a3 c3              ret

     然后再看看release的代码:

    0:003> !u 00a83038
    Normal JIT generated code
    JIT.Program.Main(System.String[])
    Begin 00db0070, size 
    14
    00db0070 33c0            xor     eax,eax
    00db0072 a31830a800      mov     dword ptr ds:[00A83018h],eax
    00db0077 e86c756378      call    mscorlib_ni
    +0x3275e8 (793e75e8) (System.Console.get_In(), mdToken: 0600076e)
    00db007c 8bc8            mov     ecx,eax
    00db007e 8b01            mov     eax,dword ptr [ecx]
    00db0080 ff5054          call    dword ptr [eax
    +54h]
    00db0083 c3              ret

     为什么同样的IL会在debug和release下经过JIT生成的本地代码会不同呢?这里引出了另一个问题:

    JIT做的自动优化

    我们可以到看生成的本地在release下比debug下小很多,也简洁清晰很多。这样导致整个项目的大小进一步的得到了优化,更能高效的执行本地代码。举个例子:

    00db0075 833d082ea80000  cmp     dword ptr ds:[0A82E08h],0
    00db007c 
    7405            je      00db0083
    00db007e e8c4823779      call    mscorwks
    !CorLaunchApplication+0x972d                  (7a128347) (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
    00db0083 
    90              nop

    这里对Main方法的参数个数与0比较,如果比0小就跳到00db0083,而00db0083那行的代码是:nop,nop表示什么都不做,这样不但会增加代码的大小还会影响代码的执行速度。JIT经常会通过识别IL中的模式来进行优化,大家平时多看看本地代码,也许比只看IL能学到更多的东西。如果大家就觉得上面的代码就只能看到这么多,其实是错的。现在引出了我今天讲的最后一个问题:

    JIT的方法inline

    我们看看release的本地代码

    00db0070 33c0            xor     eax,eax
    00db0072 a31830a800      mov     dword ptr ds:[00A83018h],eax

    这里第一句就直接得出方法的放回结果,然后将结果存到静态变量i。这就实现了方法的inline。我们知道:所有的方法调用都会带来开销。例如,需要将参数推入堆栈中或存储在寄存器中,需要执行的方法起头 (prolog) 和结尾 (epilog) 等。只需要将被调用方法的方法主体移入调用方的主体,就可以避免某些方法的调用开销。这一操作称为方法内联。JIT 使用大量的探测方法来确定是否应内联某个方法。那么是不是每个方法都能inline呢?通过msdn的介绍我们知道以下方法是不能通过内联的:

    IL 超过 32 字节的方法不会内联。

    虚函数不会内联。

    包含复杂流程控制的函数不会内联。复杂流程控制是除 if/then/else 以外的任意流程控制,在这种情况下,为 switch while

    包含异常处理块的方法不会内联,但是引发异常的方法可以内联。

    如果某个方法的所有定参都为结构,则该方法不会内联

    有兴趣的朋友可以自己验证一下。一般情况下,属性的 Get Set 方法都非常适合内联,因为它们主要用于初始化私有数据成员。但是我们不要为了内联就故意改变我们平时写代码的风格。我们必须使你的代码先工作起来,你必须清楚的知道哪些代码是不值得优化的,同时你总是应该把判断的依据建立在对速度的实际测量上,而非仅仅是阅读代码,最后,其实数据结构的选择比底层的优化重要的多。至于JIT是怎么怎么启动的,CLR通过jitinterface.cpp文件的CallCompileMethodWithSEHWrapper方法进入JIT的,还有JIT是怎么通过地址一步一步调用方法的。这些问题有兴趣的朋友可以将上面的示例代码的main方法改为Print(),然后结合windbg一起调试就很好理解。

    以上是这次要讲解的内容。

    欢迎大家指出问题。

    待续。。。 

    版权所有归"布衣软件工作者".未经容许不得转载.
  • 相关阅读:
    pytesser模块WindowsError错误解决方法
    Django 1.10中文文档-聚合
    Django 1.10中文文档-执行查询
    Python NLP入门教程
    Django1.10中文文档—模型
    曲线点抽稀算法-Python实现
    Python判断文件是否存在的三种方法
    epoll原理
    多线程编程
    后端知识地图
  • 原文地址:https://www.cnblogs.com/gjcn/p/1341431.html
Copyright © 2011-2022 走看看