熱修復設計之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

相關文章
相關標籤/搜索