最新消息: 新版網站上線了!!!

華為方舟編譯器,讓安卓性能起飛

我們都知道,Java的字節碼需要運行在Java虛擬機(JVM)上。JVM最重要的功能有兩個:執行字節碼和內存管理;我們分頭來說說。

敲黑板,先來講幾個術語:

1. JIT

全稱是Just-in-time,即時編譯;當Java字節碼運行在JVM上的時候,JVM實時得把字節碼編譯成機器碼就叫JIT。

2. AOT

全稱是Ahead-of-time,預先編譯;與JIT對應,你JIT不是實時的嗎?那我先提前編譯好,就是AOT。

3. IR

全程是Intermediate representation,即中間表示。中間表示是一個從原始表示到目標表示之間的中間層。

現代編譯器分為前端和后端,前后端的分界線就是IR。

現代編譯器的大致流程:詞法分析->語法分析->語義分析->IR->優化->生成目標代碼。

針對華為給出的方舟編譯器的講解,我們來看看方舟到底做了什么,以及推測一下方舟可能做了什么,或者方舟可以做什么。

1. 無需虛擬機運行

我們都知道,Java的字節碼需要運行在Java虛擬機(JVM)上。JVM最重要的功能有兩個:執行字節碼和內存管理;我們分頭來說說。

執行字節碼

當JVM運行字節碼的時候,會讀取一條一條的指令,然后把指令翻譯成當前機器的機器碼并執行該操作,比如把當前棧上的兩個數加起來然后再次壓棧等等,這種方式叫做解釋執行。

當JVM發現某一些指令經常會被執行到,每次翻譯一遍會導致運行效率降低,于是JVM就把這些指令直接編譯成當前機器的機器碼,下次就直接執行機器碼,不需要逐句翻譯一遍,這就是JIT。

內存管理

寫C代碼的同學們,想要使用內存的時候,需要調用malloc函數動態申請一段內存,不再使用這段內存的時候,需要調用free函數進行內存釋放,如果不釋放,后果很嚴重。

而寫Java代碼的同學們就沒有這個困惑,因為這件事被JVM承包了下來。JVM在執行字節碼的過程中,會調用gc(garbage collection),gc幫我們釋放不需要的內存。

方舟是怎么做的?

清楚了以上過程,我們就明白方舟編譯器是怎么做的了。

既然JVM可以在運行過程中可以把字節碼編譯為機器碼(JIT),那么為什么不能在運行字節碼之前把字節碼編譯成機器碼呢?沒錯,方舟就是這么做的,我們稱之為AOT。

JVM的兩大功能之一執行字節碼就不需要了,那還有一個內存管理的功能怎么辦呢?這個也好辦,華為可以提供一個庫,這個庫實現gc所有的功能,我們稱這個庫為runtime。

以前我們使用JVM來運行一段字節碼,現在這個流程變了,變成先把字節碼(或者源程序)編譯成機器碼,然后帶上runtime,直接運行在操作系統上,就不再需要VM了。

VM是不需要了,runtime是必不可少的,這個runtime需要處理包括但不限于以下幾件事:創建對象,gc,函數調用,異常處理,鎖,同步,多線程,反射。

都已經帶上了這么多功能,那再帶上一個解釋器吧,多一個不嫌多。這些東西好像有些耳熟啊,好像安卓的ART也是這樣的?我猜是的,由于Java語言本身和Java的運行時庫等等一些歷史原因,想推翻重來把這些東西都去掉,復雜度是很高的;所以安卓的爸爸谷歌也是在這些基礎上進行修修補補。

當然,華為也可以選擇不支持Java中一些動態的特性比如反射等功能,那么這個runtime是有可能簡化的。到底方舟編譯器和安卓已有的ART有什么不同,我們拭目以待。

2. 多語言聯合優化編譯器

這個很神奇對吧,C語言竟然可以和Java語言聯合在一起編譯。

我們知道C語言的代碼編譯過后是二進制文件,Java語言的代碼編譯過后是字節碼;其實現代編譯器在編譯過程中有很多層中間表示,如果把源代碼層看做最高層次,目標語言看成最低層次,編譯過程中是逐層下降的,最后下降到目標層,和我們下樓梯是一樣一樣的,并不是自由落體對不對。

比如源代碼經過編譯器前端之后變成抽象語法樹(AST),抽象語法樹又可以轉變為另一種更低層級的中間表示(IR),然后從IR再到目標層。

所以方舟可以定義一個中間表示(IR),把C語言和Java語言都先編譯到這個中間表示層,然后在中間表示層做一系列的優化或者分析,再從中間表示層編譯到機器碼,這樣就實現了多語言聯合編譯。

是不是把不同的語言編譯到同一種IR上就萬事大吉了呢?不是這樣的!

方舟為什么要把多個語言放在一起編譯?是好玩嗎?當然不是!多個語言聯合編譯至少有以下幾點好處:

減小跨語言調用開銷

不同的語言之間,類型系統、調用規范、數據布局等等都不同,所以不同語言相互調用時有一些額外的開銷。

我們知道Java調用C的接口規范叫做JNI,JNI幫助我們跨越語言的鴻溝,實現Java和C相互之間的調用。AOT在跨越語言鴻溝方面有一些好處,不同語言用同一個IR表示,runtime也是自己定制的,這不就是前店后廠嘛;

這樣就有機會抹平不同語言之間的差異,比如可以讓Java對象的數據布局和C中的對象數據布局保持一致,比如可以讓C來兼容Java的類型系統(Java語言可以看做C++語言的一個子集)等等;提前抹平差異,使不同的東西保持一致,就不必在運行程序的時候再次進行轉換,可以減小開銷。

跨語言優化

一般情況下,不同的語言是分開編譯的。而方舟編譯器將不同的語言編譯到同樣的IR,便于將不同語言的代碼聯合起來進行全局優化,比如常量傳播,函數內聯等等。

當所有的代碼都在同一IR上之后,還可以針對Java語言的特性做一些特定的靜態分析,通過分析結果進行特定優化,比如可以針對不同種類的函數調用做de-virtualization等等。

什么是de-virtulization?簡單來講就是一些函數調用是通過類似于函數指針調用的方式間接調用,分析清楚這些間接調用可以把一些間接調用改成直接調用,而且是跨語言的直接調用,神奇吧!

3. 更高效的內存回收機制

內存回收是一個大問題,安卓應用卡頓部分原因就在內存回收。

前面提到,Java的內存回收工作被JVM接管了,寫Java代碼的同學并不需要手動進行內存回收,JVM會在“適當”的時候進行內存回收。

這個“適當”的時候通常是沒有辦法的時候,內存耗盡的時候;好比我有一張干凈的桌子(堆內存),我們在桌子上面擺放了一些東西(消耗內存),當沒有地方可以擺放新東西的時候,那就需要媽媽來幫忙收拾桌面了(內存回收)。

JVM中的GC如何判斷哪些內存是需要的哪些內存是不需要的呢?這里面有個叫可達性分析的技術來幫我們判斷哪些內存可以回收。

可達性分析的大致思想是,JVM運行過程中,創建了很多對象,這些對象之間有復雜的依賴關系,JVM先確定一些對象是根對象,從根對象出發,把所有直接依賴的對象和間接依賴的對象都標記出來,沒有被依賴到的對象就不需要使用了,可以進行回收。

當有一段程序,在循環中大量創建新的對象,會造成內存快速耗盡,然后觸發gc進行內存回收;頻繁觸發gc回收大量內存,這種現象叫做內存抖動,是造成安卓應用卡頓的一個很重要的原因。

寫iOS應用的同學說我也沒有管理內存,但是我寫的應用就如絲般順滑。是的,iOS應用較少發生內存抖動現象,使用了一種叫做引用計數的方法,其實這也是可達性分析技術里面的一種,Objective-C中稱之為ARC。

引用計數是這樣一種算法,每個對象都有一個計數器,當創建對象時候或者有其它的對象引用這個對象的時候,計數器數字也加1;當別的對象不再引用它時,計數器數字減1。

當計數器的數字回到0時,就將該對象回收。

還是剛才那個循環,在循環中創建大量對象,只要本次循環結束,就可以回收剛剛創建的對象,不會造成內存抖動。

對引用計數進行加1的動作好理解,這是用戶自己寫的代碼,用戶的代碼中會寫清楚什么時候創建對象,什么時候有了新的引用;對引用計數進行減1是誰來做的呢?

這個時候編譯器就派上用場了,編譯器可以分析對象的生命周期,在合適的地方插入這個對象減1的代碼,這樣在程序運行的時候引用計數就會加加減減。

方舟編譯器的宣傳材料中提到“隨用隨回收”,那么應該是使用了引用計數類似的技術,來減小內存抖動。當然,由于Java語言的問題,引用計數并不能解決所有問題,即使使用了引用計數,也需要gc來幫助回收內存。宣傳材料中“回收時無需暫停應用”,應該是實現或者改進了Concurrent GC,來盡可能減小應用的停頓。

通過引用計數和改進GC,可以優化內存回收,減少內存回收的次數和減少暫停時間;既然有了統一的IR是不是可以天馬行空一下,除了以上的東西可不可以做更多的一些優化呢?

前面提到引用計數可以解決局部變量用完馬上回收的問題,而全局變量就搞不定了。那么方舟編譯器有可能可以在這方面做一些文章,比如可以通過分析把一部分全局變量變成局部變量;再比如可以分析全局變量的生存周期,對全局變量也進行引用計數。總之,立即釋放更多不需要使用的內存,就可以減少GC,減少卡頓。


轉載請注明:谷谷點程序 » 華為方舟編譯器,讓安卓性能起飛

体彩25选5开奖号码