深入理解.NET 的JIT编译方式

编译

CLR只执行本机的机器代码。有两种方式产生本机的机器代码:实时编译(JIT)和预编译方式(产生native image)。下面,我想谈谈JIT。
CLR使用类型的方法表来路由所有的方法调用。类型的方法表由多个入口项组成。每个入口项指向一个唯一的存根例程(stub routine)。初始化时,每个存根例程包含一个对于CLR的JIT编译器的调用(它由内部的PreStubWorker程序公开)。在JIT编译器生成本机代码后,它会重写存根例程,插入一个jmp指令跳转到刚才JIT编译器的代码。只有当要调用某个方法时,JIT编译器才会将CIL的方法体编译为相应的本机机器码版本。这样可以优化程序的工作集。
对于如下的例子:
//using System;
public class Bob{
static int x;
static void a(){x+=2;}
static void b(){x+=3;}
static void c(){x+=4;}
public static void f()
{
c();
b();
a();
}
}
public class MyClass
{
public static void Main()
{

Bob.f();
}
}
用调试器进行JIT调试。

首先,看一下每个方法的汇编显示:
Main()的汇编显示为:
push ebp
mov ebp,esp
//调用Bob.f()方法
call dword ptr ds:[00975394h]
nop
pop ebp
ret

[注]00975394h是Bob.f()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址,该地址中的内容是对应的存根例程的开始地址。

f()的汇编显示为:
push ebp
mov ebp,esp
//调用Bob.c()方法
call dword ptr ds:[00975390h]
//调用Bob.b()方法
call dword ptr ds:[0097538Ch]
//调用Bob.a()方法
call dword ptr ds:[00975388h]
nop
pop ebp
ret
[注]00975390、0097538c、00975388分别为Bob.c()、Bob.b()、Bob.a()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址,该地址中的内容是对应的存根例程的开始地址。

c()的汇编显示为:
push ebp
mov ebp,esp
add dword ptr ds:[0097539Ch],4
nop
pop ebp
ret
[注]0097539c是Bob.x的内存地址。

b()的汇编显示为:
push ebp
mov ebp,esp
add dword ptr ds:[0097539Ch],3
nop
pop ebp
ret
[注]0097539c是Bob.x的内存地址。

a()的汇编显示为:
push ebp
mov ebp,esp
add dword ptr ds:[0097539Ch],2
nop
pop ebp
ret
[注]0097539c是Bob.x的内存地址。

下面,让我们看看调试时,地址为00975394h、00975390h、0097538ch、00975388h中的内容:
0x00975384 2b 85 bf 79 03 53 97 00 13 53 97 00 23 53 97 00
0x00975394 33 53 97 00 43 53 97 00 00 00 00 00 00 00 00 00
绿色的是Bob.f()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
紫色的是Bob.c()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
灰色的是Bob.b()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
黄色的是Bob.a()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
[注]红色的内容则是Bob.x的值哦!不信吗?那你看看下面的调试过程中红色处的值的变化,就明白了:
进入f()前为:
0x00975384 2b 85 bf 79 03 53 97 00 13 53 97 00 23 53 97 00
0x00975394 33 53 97 00 43 53 97 00 00 00 00 00 00 00 00 00
c()加4后变为:
0x00975384 2b 85 bf 79 03 53 97 00 13 53 97 00 23 53 97 00
0x00975394 33 53 97 00 43 53 97 00 04 00 00 00 00 00 00 00
加3后变为:
0x00975384 2b 85 bf 79 03 53 97 00 13 53 97 00 23 53 97 00
0x00975394 33 53 97 00 43 53 97 00 07 00 00 00 00 00 00 00
加2后变为:
0x00975384 2b 85 bf 79 03 53 97 00 13 53 97 00 23 53 97 00
0x00975394 33 53 97 00 43 53 97 00 09 00 00 00 00 00 00 00

下面让我们看看在调用Bob.f()之前存根例程处的内容:
0x00975303 e8 d0 52 7d ff 04 00 10 00 50 20 00 c0 02 00 fe
0x00975313 e8 c0 52 7d ff 05 00 10 00 6c 20 00 c0 03 00 fc
0x00975323 e8 b0 52 7d ff 06 00 10 00 88 20 00 c0 04 00 fa
0x00975333 e8 a0 52 7d ff 07 00 10 00 a4 20 00 c0 05 00 f8
绿色处是Bob.f()的存根例程的内容;
紫色处是Bob.c()的存根例程的内容;
灰色处是Bob.b()的存根例程的内容;
黄色处是Bob.a()的存根例程的内容;

下面让我们看看在进入Bob.f()方法体之后存根例程处的内容:
0x00975303 e8 d0 52 7d ff 04 00 10 00 50 20 00 c0 02 00 fe
0x00975313 e8 c0 52 7d ff 05 00 10 00 6c 20 00 c0 03 00 fc
0x00975323 e8 b0 52 7d ff 06 00 10 00 88 20 00 c0 04 00 fa
0x00975333 e9 40 ad 39 06 07 00 10 00 78 00 d1 06 05 00 f8
易见,只有Bob.f()的存根例程的内容起了变化。这说明,JIT编译器被调用了。同时,编译器将f()的CIL方法体转换为地址空间中的机器码版本。再替换了存根例程的原内容。替换后,f()方法的机器码版本的首地址是0x06d10078(蓝色标注处的内容)。你不信吗?那好,我们看一下0x06d10078处的内存内容:
0x06D10078 55 8b ec ff 15 90 53 97 00 ff 15 8c 53 97 00 ff
0x06D10088 15 88 53 97 00 90 5d c3 00 00 00 00 00 00 00 00
你不妨再回头看看f()的汇编显示,这里:
紫色处不正是对Bob.c()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
灰色处不正是对Bob.b()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
黄色处不正是对Bob.a()在CORINFO_CLASS_STRUCT该内部数据结构中对应的内存地址;
不正好对应了f()汇编显示中:
//调用Bob.c()方法
call dword ptr ds:[00975390h]
//调用Bob.b()方法
call dword ptr ds:[0097538Ch]
//调用Bob.a()方法
call dword ptr ds:[00975388h]

明白了吧!好了,下面对于c() 、b()、a()的调用也是一样的道理。对于接下来的调试会有如下的内存显示:
接下来的存根例程的内容:
进入c()后变为:
0x00975303 e8 d0 52 7d ff 04 00 10 00 50 20 00 c0 02 00 fe
0x00975313 e8 c0 52 7d ff 05 00 10 00 6c 20 00 c0 03 00 fc
0x00975323 e9 78 ad 39 06 06 00 10 00 a0 00 d1 06 04 00 fa
0x00975333 e9 40 ad 39 06 07 00 10 00 78 00 d1 06 05 00 f8
进入b()后变为:
0x00975303 e8 d0 52 7d ff 04 00 10 00 50 20 00 c0 02 00 fe
0x00975313 e9 a8 ad 39 06 05 00 10 00 c0 00 d1 06 03 00 fc
0x00975323 e9 78 ad 39 06 06 00 10 00 a0 00 d1 06 04 00 fa
0x00975333 e9 40 ad 39 06 07 00 10 00 78 00 d1 06 05 00 f8
进入a()后变为:
0x00975303 e9 d8 ad 39 06 04 00 10 00 e0 00 d1 06 02 00 fe
0x00975313 e9 a8 ad 39 06 05 00 10 00 c0 00 d1 06 03 00 fc
0x00975323 e9 78 ad 39 06 06 00 10 00 a0 00 d1 06 04 00 fa
0x00975333 e9 40 ad 39 06 07 00 10 00 78 00 d1 06 05 00 f8

进入c()后地址为0x06d100a0的内容(Bob.c()的机器码版本)如下:
0x06D100A0 55 8b ec 83 05 9c 53 97 00 04 90 5d c3 00 00 00
进入b()后地址为0x06d100c0的内容(Bob.b()的机器码版本)如下:
0x06D100C0 55 8b ec 83 05 9c 53 97 00 03 90 5d c3 00 00 00
进入a()后地址为0x06d100e0的内容(Bob.a()的机器码版本)如下:
0x06D100E0 55 8b ec 83 05 9c 53 97 00 02 90 5d c3 00 00 00

时间: 2024-09-12 05:00:48

深入理解.NET 的JIT编译方式的相关文章

Java HotSpot VM中的JIT编译

原文地址译者:郭蕾 校对:丁一 本文是Java HotSpot VM and just-in-time(JIT) compilation系列的第一篇. Java HotSpot虚拟机是Oracle收购Sun时获得的,JVM和开源的OpenJDK都是以此虚拟机为基础发展的.如同其它虚拟机,HotSpot虚拟机为字节码提供了一个运行时环境.实际上,它主要会做这三件事情: 执行方法所请求的指令和运算. 定位.加载和验证新的类型(即类加载). 管理应用内存. 最后两点都是各自领域的大话题,所以这篇文章中

你的Java代码对JIT编译友好么?(转)

JIT编译器是Java虚拟机(以下简称JVM)中效率最高并且最重要的组成部分之一.但是很多的程序并没有充分利用JIT的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用JIT的程度. 在本文中,我们将介绍一些简单的方法来验证你的程序是否对JIT友好.这里我们并不打算覆盖诸如JIT编译器工作原理这些细节.只是提供一些简单基础的检测和方法来帮助你的代码对JIT友好,进而得到优化. JIT编译的关键一点就是JVM会自动地监控正在被解释器执行的方法.一旦某个方法被视为频繁调用,这个方法就会被标记

你的 Java 代码对 JIT 编译友好么?

JIT编译器是Java虚拟机(以下简称JVM)中效率最高并且最重要的组成部分之一.但是很多的程序并没有充分利用JIT的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用JIT的程度. 在本文中,我们将介绍一些简单的方法来验证你的程序是否对JIT友好.这里我们并不打算覆盖诸如JIT编译器工作原理这些细节.只是提供一些简单基础的检测和方法来帮助你的代码对JIT友好,进而得到优化. JIT编译的关键一点就是JVM会自动地监控正在被解释器执行的方法.一旦某个方法被视为频繁调用,这个方法就会被标记

[WebKit] JavaScriptCore解析--基础篇(三)从脚本代码到JIT编译的代码实现

前面说了一些解析.生成ByteCode直至JIT的基本概念,下面是对照JavaScriptCore源代码来大致了解它的实现. 从JS Script到Byte Code 首先说明Lexer, Parser和ByteCode的生成都是由ProgramExecutable初始化过程完成的.先在JSC的API evaluate()中会创建ProgramExecutable并指定脚本代码.然后传入Interpreter时,再透过CodeCache获取的UnlinkedProgramCodeBlock就是已

JIT编译

编译 在MSIL执行前,必须通过.NET框架的JIT编译器编译成本地代码,这个本地代码是基于特定CPU的,而且必须和JIT运行在同一套计算机体系结构上.由于运行时对于其运行的每一个CPU架构都提供了一个JIT编译器,开发人员可以编写一套可以通过JIT编译从而运行在不同架构的计算机上的MSIL代码(如果你的受控代码调用了特定平台的API或者类库,那么你的代码就只能在特定的计算机上执行了).    JIT编译的一个想法是基于认识到一些代码可能在执行过程中从来都没有被调用过这样一个事实,因而与其花费时

Oracle存储过程本地编译方式

  通常将Oracle存储过程编译为本地编译方式的测试记录. 测试用表: ? 1 2 3 4 5 6 7 SQL> create table t1(rid number);    Table created    SQL> create table t_n(rid number);    Table created 测试用的存储过程: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 create or replace procedure pro_xcl(p1 varchar2)

IntelliJ IDEA 编译方式介绍

编译方式介绍 相比较于 Eclipse 的实时自动编译,IntelliJ IDEA 的编译更加手动化,虽然 IntelliJ IDEA 也支持通过设置开启实时编译,但是不建议,因为太占资源了.IntelliJ IDEA 编译方式除了手工点击编译按钮进行编译之外,还有就是在容器运行之前配置上一个编译事件,先编译后运行.默认下 IntelliJ IDEA 也都是这样的设置,所以实际开发中你也不用太注意编译这件事.虽然 IntelliJ IDEA 没有实时编译,但是对于代码检查完全是没有影响.但是多个

Java的三种编译方式

通常Java有三种编译方式,编译方式不同,那么得到的.class的大小也不同. 1)默认编译方式:javac A.java 2)  调试编译方式:javac -g A.java 3)  代码编译方式:javac -g:none A.java 案例如下:类A public class A{ public static void main(String args[]){ for(int i=0;i<100000;i++){ A a = new A(); } } } 通过上面这三种编译方式,得到的.c

JVM深入学习笔记二:Java JIT编译

JIT是java虚拟机把热点字节码编译成机器码的技术. 解释执行,在当运行次数比较少的时候能够省去编译的操作直接运行字节码.  另外解释更加的节约内存. 而编译为机器码则可以获得更高的效率. 因为各有好处,HotSpot使用了共存的机制,可以使用-Xint强制使用解释模式或者是-Xcomp 编译模式. 此外HotSpot提供了两种编译器Client Compile和Server Compiler,分别针对于更快的编译速度和更好的编译效果.使用-client或者-server参数指定 在这种共存模