热修复设计之AOT/JIT&dexopt 与 dex2oat (一)

 

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将先从AOT/JIT&dexopt 与 dex2oat来介绍热修复设计:html

1、AOT/JIT

一个程序的编译过程能够是步骤迭代式的,即每一轮步骤结束后获得的结果均可独立运行,好比,先构造AST再输出字节码,中间状态AST也是能够解释执行的。因为编译的本质就是代码转换,所以对一个语言能够有多个独立的编译器,每一个负责一轮步骤java

AOT Compiler和JIT Compiler就是针对编译形式作的分类:
AOT:Ahead Of Time,指在运行前编译,好比普通的静态编译
JIT:Just In Time,指在运行时编译,边运行边编译,好比java虚拟机在运行时就用到JIT技术python

JIT可能知道的人多些,AOT这个名词就相对少见一些了,其实除了JIT,剩下的都是AOT。wiki上JIT的解释也比AOT详尽不少,若是按wiki上的理解,通常来讲,是从形式上来区分这两个概念,即看编译是否是在“运行时”进行程序员

然而,这两个概念又有模糊性,问题在于这个“运行时”怎么来区分,比方说,从这个概念来看,python是用到JIT技术的,由于:算法

... 
import a 
...

当执行到import a的时候,固然是运行时,这时候若是只找到了a.py,则会进行编译工做,并生成a.pyc,这就是python的JIT特性,可是通常来讲,认为python的JIT是psyco、pypy之类,并不认为python自己的动态性属于JIT范畴,或者说,它的这种“形式上”的JIT特性不归入讨论范围。其余脚本语言,动态语言也有相似的状况。具体缘由我以为有几点
首先被主流理论认定的JIT编译器对于被其编译的语言来讲属于附加品,也就是说,就算去掉JIT,并不影响语言自己的运行,例如java,若是关闭JIT,依然能够解释执行,而上述python的运行时import的特性虽然形式上符合JIT,但这个机制是语言自己规定的,若是去掉,语言(的主流实现)就不完整了。反过来讲,若是python采用源码直接解析执行,则编译为字节码的行为就能够看作是JIT,由于作不作都不影响解析执行过程
其次,python的这种编译并不是每次执行都会进行,由于通常来讲会生成字节码结果pyc文件存在磁盘,它更像是对java源代码转class文件这一过程的惰性化,在须要的时候进行
最后,JIT会消耗运行时资源,可能致使进程卡顿,而java等语言之因此引入JIT,是由于JIT对字节码编译后能以更快的速度运行,卡顿的时间能补救回来,所以从工程角度讲,JIT几乎就等因而运行时优化(虽然从概念和形式上并不是如此),而python的import就只有卡顿,对速度没啥好处
因而,虽然从概念来讲,上面的例子的确符合JIT,但通常来讲也不这么认为,出发角度问题,说python自带JIT特性或没有JIT都算说得通的服务器

之因此先举这个例子,由于我以为能体现AOT和JIT概念的对立和统一,对立是形式上的,以“运行”为分界线,而统一则是说,其实全部须要执行的指令序列,都是须要先编译再执行的,好比import a,这个相对于整个进程固然是JIT,但相对于a.py这个模块(python进程首次import某个模块时会执行它)不妨看作AOT,若是有人以为这么作不妥,那换个更明显的例子,若是一个python程序的全部import都在进程开启时当即运行,而后才进入执行,那按照概念来讲,这是JIT,由于进程已经开始运行了,可是,为何不能看作是先编译再执行的AOT模式,只是整个过程被批处理化了呢?网络

带着这个问题再考虑不少资料(包括wiki)对JIT的另外一个描述,JIT是在运行时将解释执行的语言(好比字节码)编译成机器指令,以提升运行速度。这个见解在前面的某篇也提过,的确不少JIT编译器,好比java的就是这么干的(咱们下面就拿java举例),可是,既然字节码编译成机器指令能够提升速度,为什么必定要放在运行时进行,作成AOT模式不是能够运行得更流畅吗,并且还能一次编译,N次执行,为啥非要作成运行时作,JIT原本是要提升运行速度,但这岂不是下降了效率?架构

这种见解是有道理的,事实上,java的确有一些AOT编译器,能够将字节码甚至java源码直接编译成机器指令的可执行文件,微软当初的VJ++彷佛就这么搞的,和sun打了好久的架,sun还喊出了pure java(纯粹的java,即按照sun的设计理念和标准来实现java)的口号,有兴趣能够去搜一下这段历史,挺搞笑的app

另外一方面,sun的jvm虽然采用了JIT编译,但同时也提供了client和server模式,在server模式下,虚拟机在一开始执行的时候会先尽量多地对字节码进行编译,且优化程度也尽可能高,这样可使得服务器在运行过程当中能尽可能少卡顿,根据上面的讨论,这实际上至关于AOT批处理了。client模式下则不会这样作,主要是为了尽可能缩短启动延迟,提升用户体验jvm

顺便说一句,对于JIT将字节码编译成机器指令,wiki的描述比较暧昧,有时候用machine code,有时候用native code,比方说咱们用java实现一个A语言的虚拟机,解释A的字节码执行,并将字节码编译成java本身的字节码,这也是JIT,由于A跑在jvm上,则java字节码就看作是native code,而machine code这个machine也不见得是真实机器,jvm也是一种机器

因为JIT编译耗费运行时间,则对于某些优化点就没法作到百分百支持,必须在代码优化和执行卡顿之间作一个权衡,AOT就没有这个问题,另外,AOT能够作到编译后持久化到存储,而JIT通常是每运行一次就会搞一遍重复的编译

若是咱们不考虑AOT自己耗费的时间(好比编译一次,N次运行),也不考虑使用上的方便性(AOT可能会有屡次编译过程),那是否是能够认为,AOT编译能够彻底替换JIT编译,JIT就彻底不必了,实际状况固然不是这样,JIT仍是有它的优点和必要性的,不然研究它的那群人岂不都是傻子

从动静态来看这个问题,AOT是静态编译,而JIT是运行时动态编译,则JIT的优点在于,它不但能看到静态信息(代码),还能看到运行时的状况,这就是JIT的优点。接下来讨论的JIT是一种狭义的JIT,即在AOT搞不定的地方使用的JIT,而非上述形式上的

关于JIT的优点,wiki上给出了四点理由,但有意思的是,其中有两条连它本身都认可并不是只有JIT能作,也就是说至少理论上,用AOT实现(或部分实现)是可行的,这四条是:
一、JIT能够根据当前的硬件状况实时编译成最优机器指令,好比cpu中若是含FPU,MMX,SSE2,或者Intel cpu的并行计算特性,则能够作到同一份字节码,在不一样机器运行时最大限度利用硬件资源。而若是是AOT编译一个程序放出去给不一样用户使用,就只能去兼容特性最少的cpu,或者内部实现多个版本
二、JIT能够根据当前进程实际运行状态,将字节码编译成适合最优化的机器指令序列。wiki认为静态编译也能够经过分析profile来实现这方面的优化(可能有点麻烦)
三、当程序须要支持动态连接时,即在静态编译阶段,可能不知道运行时会引入什么样的代码来和程序协做执行,这时候就只能依靠JIT
四、考虑到垃圾收集,JIT能够根据进程中的内存实际状况来调整代码,使得cache能更充分地使用,wiki认为静态编译也能够作到,但JIT作起来更容易实现

对于第一条,JIT的确能够实现这种优化,可是AOT同样能够实现,虽然AOT编译一个程序给不一样用户执行没法作到,可是能够编译字节码发布,用户使用时再根据当前机器再作一次AOT
对于第二条,首先我认为大多数程序的运行状态不会常常变更,好比同一个程序有时候是整数计算居多,有时候是浮点计算居多,通常来讲程序应用场景是固定的;其次对于特定场景也能够AOT
对于第三条,的确动态连接的全文静态优化AOT没法作到,可是如上篇所说,必要时候咱们能够直接砍掉语言的动态性,再者静态编译时候也不是什么都感知不到,好比C语言作静态连接时,至少是知道头文件的,动态性没那么强
对于第四条,AOT也是有可能实现的,虽然麻烦不少。另外一方面,静态编译时也有指令乱序来提升cache使用效果,再者这块也和垃圾收集算法、程序自己的局部性有很大关系,若是程序自己写的烂,这个调整效果可能也比较有限

因此我以为,这四条虽然都有道理,但没精确说到点子上。再来审视这个问题,咱们能够看出,从理论上讲,AOT能够彻底代替JIT,由于一个进程的状态是有限的,AOT能够预测全部可能状况并进行优化,实际运行时的状态不会超出AOT的预测,采用最优代码执行便可,而JIT在这里的优点就是,它能精准地得知运行时状态,而不是像AOT那样预测,成本更低,若是一个AOT优化的成本太高,则应该选择JIT。AOT不是不能作,而是不可行

JIT相关的资料,相比wiki我更推荐这篇论文:《Representation-based Just-in-time Specialization and the Psyco prototype for Python》 by Armin Rigo,这个论文是以python和其JIT插件库psyco为例来分析,论文题目中的单词Specialization可谓画龙点睛,它指出至少在动态类型语言中,JIT的关键做用之一是特化,用上篇的话说,就是动态行为静态化,而这些场景中AOT不可行的缘由是它很难找到特化的方向,而枚举全部特化是不可行的

一个典型的特化案例,也是论文中提到的,假设有一个函数f(x,y),则对于x的输入x1,x2,x3...,咱们能够特化这个函数为f1(y),f2(y),f3(y)...,其中fk(y)在功能上对应f(xk,y),这样一来,每一个fk能够单独地作优化,与其余函数无关,而特化后的函数列表至少不会比原来的f(x,y)慢。惟一的问题是,x的取值可能不少,好比x是一个int,则若是采用AOT方式来特化,则须要编译42亿多个函数,这显然是不现实的,可是JIT就有可能对这个场景作优化,缘由在于,x的取值虽然不少,但在一个具体运行过程当中范围相对小,甚至是很小,这符合二八定律

因而,在运行时咱们能够对函数f作监控,统计每次输入的x的值,若是发现这些值的分布不平均,好比x为123的状况占大多数,则动态特化一个f123(y),对其进行高度优化,而后修改f函数为:

func f(x, y): 
    if x == 123: 
        return f123(y) 
    ... //f的正常流程

因而只须要一个特化函数,就能带来运行时效率的提高,这就是JIT特化的优点

对不少程序来讲,对这种数值作监控和特化可能性价比不高,由于不是每一个函数的输入值范围都呈现不平衡状态,或者说不是那么明显,但上面这个例子中,x和y不必定是变量,也能够是类型,这样一来对动态类型语言就有很大的意义

前面讲过,在C++中能够用模板来实现鸭子类型,实质是经过代码替换来实现类型静态化,C++这个方式虽然效率高,但渠道是经过静态编译中的全文分析,是AOT编译,若是改为稍微动态性强一些的语言,就用不上了。在动态类型中,一个函数若是有k个参数,有n个可能类型,则AOT须要将一个函数扩展为n^k个特化实例,n和k稍大一点就不可操做了,况且自己就是动态类型,n的范围都不必定在编译期能知道

对这种场景,JIT就能够经过统计的方式来选择性地特化,这个的可行性和现实意义更大,缘由在于,程序员在用动态类型写程序的时候,好比写一个函数:

func f(x, y): 
    return x + y

理论上,这个函数能够接受任意类型的x和y,只要x能和y相加便可,但具体到一个肯定的程序,这个函数的业务意义通常是固定的,或者是作字符串拼接,或者是数值相加,不多说写一个函数,接收八竿子打不着的不一样的类型还能运算,并且仍是程序员刻意这么设计,就像前面讲过的C++模板的二义性同样,基本见不到这种需求,因此在函数的输入参数类型上,符合二八定律。因而对于上述代码,假设x和y绝大多数状况下都是整数,则进行特化(假设这个伪代码中不考虑整数溢出):

func f(x, y): 
    if not (x instanceof int and y instanceof int): 
        //有一个不是整数,走原有流程 
        return x + y 
    //整数加法的特化流程 
    internal_code: 
        int ix = get_internal_int(x) 
        int iy = get_internal_int(y) 
        int iresult 
        asm: 
            push ... //当前状态压栈 
            mov eax, ix 
            mov ebx, iy 
            add eax, ebx 
            mov iresult, eax 
            pop ... //状态出栈 
        return build_int_object(iresult)

固然这只是个例子,若是只是为了一个加法,这多少有点小题大作,但若是f的逻辑较为复杂,优化就很明显了

还能够逆向思惟一下,AOT难以实现特化的缘由是没法考虑全部状况,但咱们也没有必要考虑全部状况,实际上类型使用的二八定律自己也在另外一个二八定律里,具体到int类型,一个绝大多数使用到的类型都是int的程序在全部程序中占绝大多数,至少在一个有限的领域是这样,所以干脆对于每一个函数都只作int相关的特化,这样2k种状况还算能接受(实际状况数比2k低不少,由于不少参数若是被假定为int,会语法错误,就不用假设了),若是再作的好一点,还能够作成编译器选项,由用户来指定AOT的时候对哪一个类型特化,这样就比较完美了

除类型的动态性外,其余动态性也能够相似讨论,仅拿上篇的例子,不赘述了:

for i in range(n): 
    print(i) 
转换为: 
if not (range is builtins.range and print is builtins.print): 
    for i in range(n): 
        print(i) 
else: 
    internal_code: 
        long tmp = get_internal_long(n) 
        long i 
        //这里应该用汇编,仅表个意思 
        for (i = 0; i < tmp; ++ i): 
            print_long(i)

须要在程序启动时在builtins里面保存默认函数,用于检测当前运行环境是否被用户修改过,这样就兼顾了效率和动态性,跟上面同样,这里JIT或AOT实现均可以。

2、dexopt 与 dex2oat 区别

从应用层开发来讲有个原理上的大体理解也是必须掌握的,具体区别可用以下图概述(图片来自网络)。

 

 
19956127-b3d84776d9e1ddc6.png
 

 

 

经过上图能够很明显的看出 dexopt 与 dex2oat 的区别,前者针对 Dalvik 虚拟机,后者针对 Art 虚拟机。

 

dexopt 是对 dex 文件 进行 verification 和 optimization 的操做,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操做码(譬如优化调用虚拟指令等)。

dex2oat 是对 dex 文件的 AOT 提早编译操做,其须要一个 dex 文件,而后对其进行编译,结果是一个本地可执行的 ELF 文件,能够直接被本地处理器执行。

除此以外在上图还能够看到 Dalvik 虚拟机中有使用 JIT 编译器,也就是说其也能将程序运行的热点 java 字节码编译成本地 code 执行,因此其与 Art 虚拟机仍是有区别的。Art 虚拟机的 dex2oat 是提早编译全部 dex 字节码,而 Dalvik 虚拟机只编译使用启发式检测中最频繁执行的热点字节码。
参考:https://www.jianshu.com/p/26a82119da49
https://blog.csdn.net/xtlisk/article/details/39099199
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

相关文章
相关标签/搜索