華為方舟編譯器做了些什么,讓安卓有了“絲滑”的感覺 ?
敲黑板,先來講幾個術(shù)語:
1. JIT
全稱是Just-in-time,即時編譯;當Java字節(jié)碼運行在JVM上的時候,JVM實時得把字節(jié)碼編譯成機器碼就叫JIT。
2. AOT
全稱是Ahead-of-time,預先編譯;與JIT對應,你JIT不是實時的嗎?那我先提前編譯好,就是AOT。
3. IR
全程是Intermediate representation,即中間表示。中間表示是一個從原始表示到目標表示之間的中間層。
現(xiàn)代編譯器分為前端和后端,前后端的分界線就是IR。
現(xiàn)代編譯器的大致流程:詞法分析->語法分析->語義分析->IR->優(yōu)化->生成目標代碼。
針對華為給出的方舟編譯器的講解,我們來看看方舟到底做了什么,以及推測一下方舟可能做了什么,或者方舟可以做什么。
1. 無需虛擬機運行
我們都知道,Java的字節(jié)碼需要運行在Java虛擬機(JVM)上。JVM最重要的功能有兩個:執(zhí)行字節(jié)碼和內(nèi)存管理;我們分頭來說說。
執(zhí)行字節(jié)碼
當JVM運行字節(jié)碼的時候,會讀取一條一條的指令,然后把指令翻譯成當前機器的機器碼并執(zhí)行該操作,比如把當前棧上的兩個數(shù)加起來然后再次壓棧等等,這種方式叫做解釋執(zhí)行。
當JVM發(fā)現(xiàn)某一些指令經(jīng)常會被執(zhí)行到,每次翻譯一遍會導致運行效率降低,于是JVM就把這些指令直接編譯成當前機器的機器碼,下次就直接執(zhí)行機器碼,不需要逐句翻譯一遍,這就是JIT。
內(nèi)存管理
寫C代碼的同學們,想要使用內(nèi)存的時候,需要調(diào)用malloc函數(shù)動態(tài)申請一段內(nèi)存,不再使用這段內(nèi)存的時候,需要調(diào)用free函數(shù)進行內(nèi)存釋放,如果不釋放,后果很嚴重。
而寫Java代碼的同學們就沒有這個困惑,因為這件事被JVM承包了下來。JVM在執(zhí)行字節(jié)碼的過程中,會調(diào)用gc(garbage collection),gc幫我們釋放不需要的內(nèi)存。
方舟是怎么做的?
清楚了以上過程,我們就明白方舟編譯器是怎么做的了。
既然JVM可以在運行過程中可以把字節(jié)碼編譯為機器碼(JIT),那么為什么不能在運行字節(jié)碼之前把字節(jié)碼編譯成機器碼呢?沒錯,方舟就是這么做的,我們稱之為AOT。
JVM的兩大功能之一執(zhí)行字節(jié)碼就不需要了,那還有一個內(nèi)存管理的功能怎么辦呢?這個也好辦,華為可以提供一個庫,這個庫實現(xiàn)gc所有的功能,我們稱這個庫為runtime。
以前我們使用JVM來運行一段字節(jié)碼,現(xiàn)在這個流程變了,變成先把字節(jié)碼(或者源程序)編譯成機器碼,然后帶上runtime,直接運行在操作系統(tǒng)上,就不再需要VM了。
VM是不需要了,runtime是必不可少的,這個runtime需要處理包括但不限于以下幾件事:創(chuàng)建對象,gc,函數(shù)調(diào)用,異常處理,鎖,同步,多線程,反射。
都已經(jīng)帶上了這么多功能,那再帶上一個解釋器吧,多一個不嫌多。這些東西好像有些耳熟啊,好像安卓的ART也是這樣的?我猜是的,由于Java語言本身和Java的運行時庫等等一些歷史原因,想推翻重來把這些東西都去掉,復雜度是很高的;所以安卓的爸爸谷歌也是在這些基礎(chǔ)上進行修修補補。
當然,華為也可以選擇不支持Java中一些動態(tài)的特性比如反射等功能,那么這個runtime是有可能簡化的。到底方舟編譯器和安卓已有的ART有什么不同,我們拭目以待。
2. 多語言聯(lián)合優(yōu)化編譯器
這個很神奇對吧,C語言竟然可以和Java語言聯(lián)合在一起編譯。
我們知道C語言的代碼編譯過后是二進制文件,Java語言的代碼編譯過后是字節(jié)碼;其實現(xiàn)代編譯器在編譯過程中有很多層中間表示,如果把源代碼層看做***層次,目標語言看成***層次,編譯過程中是逐層下降的,***下降到目標層,和我們下樓梯是一樣一樣的,并不是自由落體對不對。
比如源代碼經(jīng)過編譯器前端之后變成抽象語法樹(AST),抽象語法樹又可以轉(zhuǎn)變?yōu)榱硪环N更低層級的中間表示(IR),然后從IR再到目標層。
所以方舟可以定義一個中間表示(IR),把C語言和Java語言都先編譯到這個中間表示層,然后在中間表示層做一系列的優(yōu)化或者分析,再從中間表示層編譯到機器碼,這樣就實現(xiàn)了多語言聯(lián)合編譯。
是不是把不同的語言編譯到同一種IR上就萬事大吉了呢?不是這樣的!
方舟為什么要把多個語言放在一起編譯?是好玩嗎?當然不是!多個語言聯(lián)合編譯至少有以下幾點好處:
減小跨語言調(diào)用開銷
不同的語言之間,類型系統(tǒng)、調(diào)用規(guī)范、數(shù)據(jù)布局等等都不同,所以不同語言相互調(diào)用時有一些額外的開銷。
我們知道Java調(diào)用C的接口規(guī)范叫做JNI,JNI幫助我們跨越語言的鴻溝,實現(xiàn)Java和C相互之間的調(diào)用。AOT在跨越語言鴻溝方面有一些好處,不同語言用同一個IR表示,runtime也是自己定制的,這不就是前店后廠嘛;
這樣就有機會抹平不同語言之間的差異,比如可以讓Java對象的數(shù)據(jù)布局和C中的對象數(shù)據(jù)布局保持一致,比如可以讓C來兼容Java的類型系統(tǒng)(Java語言可以看做C++語言的一個子集)等等;提前抹平差異,使不同的東西保持一致,就不必在運行程序的時候再次進行轉(zhuǎn)換,可以減小開銷。
跨語言優(yōu)化
一般情況下,不同的語言是分開編譯的。而方舟編譯器將不同的語言編譯到同樣的IR,便于將不同語言的代碼聯(lián)合起來進行全局優(yōu)化,比如常量傳播,函數(shù)內(nèi)聯(lián)等等。
當所有的代碼都在同一IR上之后,還可以針對Java語言的特性做一些特定的靜態(tài)分析,通過分析結(jié)果進行特定優(yōu)化,比如可以針對不同種類的函數(shù)調(diào)用做de-virtualization等等。
什么是de-virtulization?簡單來講就是一些函數(shù)調(diào)用是通過類似于函數(shù)指針調(diào)用的方式間接調(diào)用,分析清楚這些間接調(diào)用可以把一些間接調(diào)用改成直接調(diào)用,而且是跨語言的直接調(diào)用,神奇吧!
3. 更高效的內(nèi)存回收機制
內(nèi)存回收是一個大問題,安卓應用卡頓部分原因就在內(nèi)存回收。
前面提到,Java的內(nèi)存回收工作被JVM接管了,寫Java代碼的同學并不需要手動進行內(nèi)存回收,JVM會在“適當”的時候進行內(nèi)存回收。
這個“適當”的時候通常是沒有辦法的時候,內(nèi)存耗盡的時候;好比我有一張干凈的桌子(堆內(nèi)存),我們在桌子上面擺放了一些東西(消耗內(nèi)存),當沒有地方可以擺放新東西的時候,那就需要媽媽來幫忙收拾桌面了(內(nèi)存回收)。
JVM中的GC如何判斷哪些內(nèi)存是需要的哪些內(nèi)存是不需要的呢?這里面有個叫可達性分析的技術(shù)來幫我們判斷哪些內(nèi)存可以回收。
可達性分析的大致思想是,JVM運行過程中,創(chuàng)建了很多對象,這些對象之間有復雜的依賴關(guān)系,JVM先確定一些對象是根對象,從根對象出發(fā),把所有直接依賴的對象和間接依賴的對象都標記出來,沒有被依賴到的對象就不需要使用了,可以進行回收。
當有一段程序,在循環(huán)中大量創(chuàng)建新的對象,會造成內(nèi)存快速耗盡,然后觸發(fā)gc進行內(nèi)存回收;頻繁觸發(fā)gc回收大量內(nèi)存,這種現(xiàn)象叫做內(nèi)存抖動,是造成安卓應用卡頓的一個很重要的原因。
寫iOS應用的同學說我也沒有管理內(nèi)存,但是我寫的應用就如絲般順滑。是的,iOS應用較少發(fā)生內(nèi)存抖動現(xiàn)象,使用了一種叫做引用計數(shù)的方法,其實這也是可達性分析技術(shù)里面的一種,Objective-C中稱之為ARC。
引用計數(shù)是這樣一種算法,每個對象都有一個計數(shù)器,當創(chuàng)建對象時候或者有其它的對象引用這個對象的時候,計數(shù)器數(shù)字也加1;當別的對象不再引用它時,計數(shù)器數(shù)字減1。
當計數(shù)器的數(shù)字回到0時,就將該對象回收。
還是剛才那個循環(huán),在循環(huán)中創(chuàng)建大量對象,只要本次循環(huán)結(jié)束,就可以回收剛剛創(chuàng)建的對象,不會造成內(nèi)存抖動。
對引用計數(shù)進行加1的動作好理解,這是用戶自己寫的代碼,用戶的代碼中會寫清楚什么時候創(chuàng)建對象,什么時候有了新的引用;對引用計數(shù)進行減1是誰來做的呢?
這個時候編譯器就派上用場了,編譯器可以分析對象的生命周期,在合適的地方插入這個對象減1的代碼,這樣在程序運行的時候引用計數(shù)就會加加減減。
方舟編譯器的宣傳材料中提到“隨用隨回收”,那么應該是使用了引用計數(shù)類似的技術(shù),來減小內(nèi)存抖動。當然,由于Java語言的問題,引用計數(shù)并不能解決所有問題,即使使用了引用計數(shù),也需要gc來幫助回收內(nèi)存。宣傳材料中“回收時無需暫停應用”,應該是實現(xiàn)或者改進了Concurrent GC,來盡可能減小應用的停頓。
通過引用計數(shù)和改進GC,可以優(yōu)化內(nèi)存回收,減少內(nèi)存回收的次數(shù)和減少暫停時間;既然有了統(tǒng)一的IR是不是可以天馬行空一下,除了以上的東西可不可以做更多的一些優(yōu)化呢?
前面提到引用計數(shù)可以解決局部變量用完馬上回收的問題,而全局變量就搞不定了。那么方舟編譯器有可能可以在這方面做一些文章,比如可以通過分析把一部分全局變量變成局部變量;再比如可以分析全局變量的生存周期,對全局變量也進行引用計數(shù)。總之,立即釋放更多不需要使用的內(nèi)存,就可以減少GC,減少卡頓。
好了,胡言亂語完了,我們還是等方舟編譯器開源了,然后再一探究竟吧。
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號coderising獲取授權(quán)】