JIT-動態編譯與AOT-靜態編譯:java/ java/ JavaScript/Dart亂談

zhoulujun發表於2021-08-24

C 和 C++ 之類的編譯語言效能遠超Java,但是生成的程式碼只能在有限的幾種系統上執行,這就有了Java的存在基礎(JVM-跨平臺)

早期 Java 執行時所提供的效能級別遠低於 C 和 C++ 之類的編譯語言。

最早的時候,java是由直譯器(Interpreter),將每個java指令轉譯為對等的微處理器指令,並根據轉譯後的指令先後次序依序執行,一個java指令可能對應十幾或者幾十個對等微處理指令,執行的時候還要先解釋,在硬體條件差的情況下,執行速度是可想而知有多慢的

後面 Java 通過 JIT編譯器(Just-in-time Compiler) 優化,開掛霸佔Web 開發頭牌幾十年。比如傍上java這個親戚的JavaScript,在V9 引擎裡通過JIT,造成前端 吼吼 Web 一條龍服務(nodeJS 全棧),感興趣可以看下《ECMAScript進化史(1):話說Web指令碼語言王者JavaScript的加冕歷史

當java執行runtime環境時,每遇到一個class,JIT就會對這個類進行編譯,生成相當精簡的二進位制碼,花費少許的編譯時間來換取後續的執行速率,這個效率提高還是比較大的,但這並沒有達到頂尖的效能,因為某些java檔案是極少執行的,編譯它們的時間有可能遠遠長於轉譯器轉譯執行的時間,整體下來,花費的時間並沒有減少。

基於JIT的經驗,又出來了動態編譯器(dynamic compiler),動態預判哪些需要compile哪些需要轉譯,所以動態編譯器是既包含了轉譯器&編譯器的。

JIT 動態編譯

儘管傳聞中 Java 程式設計的 “一次編寫,隨處執行” 的口號可能並非在所有情況下都嚴格成立,但是對於大量的應用程式來說情況確實如此。另一方面,本地編譯本質上是特定於平臺的。

那麼 Java 平臺如何在不犧牲平臺無關性的情況下實現本地編譯的效能?答案就是使用 JIT 編譯器進行動態編譯,這種方法已經使用了十年

儘管通過 JIT 編譯保持了平臺無關性,但是付出了一定代價。因為在程式執行時進行編譯,所以編譯程式碼的時間將計入程式的執行時間。任何編寫過大型 C 或 C++ 程式的人都知道,編譯過程往往較慢。

克服編譯過程慢

  1. 編譯所有的程式碼,但是不執行任何耗時多的分析和轉換,因此可以快速生成程式碼。由於生成程式碼的速度很快,因此儘管可以明顯觀察到編譯帶來的開銷,但是這很容易就被反覆執行原生程式碼所帶來的效能改善所掩蓋。

  2. 將編譯資源只分配給少量的頻繁執行的方法(通常稱作熱方法)。低編譯開銷更容易被反覆執行熱程式碼帶來的效能優勢掩蓋。很多應用程式只執行少量的熱方法,因此這種方法有效地實現了編譯效能成本的最小化。

動態編譯器的一個主要的複雜性在於權衡瞭解編譯程式碼的預期獲益使方法的執行對整個程式的效能起多大作用。一個極端的例子是,程式執行後,您非常清楚哪些方法對於這個特定的執行的效能貢獻最大,但是編譯這些方法毫無用處,因為程式已經完成。而在另一個極端,程式執行前無法得知哪些方法重要,但是每種方法的潛在受益都最大化了。大多數動態編譯器的操作介於這兩個極端之間,方法是權衡瞭解方法預期獲益的重要程度。

Java 語言需要動態載入類這一事實對 Java 編譯器的設計有著重要的影響。如果待編譯程式碼引用的其他類還沒有載入怎麼辦?

比如一個方法需要讀取某個尚未載入的類的靜態欄位值。Java 語言要求第一次執行類引用時載入這個類並將其解析到當前的 JVM 中。直到第一次執行時才解析引用,這意味著沒有地址可供從中載入該靜態欄位。

編譯器如何處理這種可能性?

編譯器生成一些程式碼,用於在沒有載入類時載入並解析類。類一旦被解析,就會以一種執行緒安全的方式修改原始程式碼位置以便直接訪問靜態欄位的地址,因為此時已獲知該地址。

IBM JIT 編譯器中進行了大量的努力以便使用安全而有效率的程式碼補丁技術,因此在解析類之後,執行的原生程式碼只載入欄位的值,就像編譯時已經解析了欄位一樣。另外一種方法是生成一些程式碼,用於在查明欄位的位置以前一直檢查是否已經解析欄位,然後載入該值。對於那些由未解析變成已解析並被頻繁訪問的欄位來說,這種簡單的過程可能帶來嚴重的效能問題。

動態編譯的優/缺點

動態地編譯 Java 程式有一些重要的優點,甚至能夠比靜態編譯語言更好地生成程式碼,現代的 JIT 編譯器常常向生成的程式碼中插入掛鉤以收集有關程式行為的資訊,以便如果要選擇方法進行重編譯,就可以更好地優化動態行為。

但是,動態編譯確實具有一些缺點,這些缺點使它在某些情況下算不上一個理想的解決方案

因為識別頻繁執行的方法以及編譯這些方法需要時間,所以應用程式通常要經歷一個準備過程,在這個過程中效能無法達到其最高值

在這個準備過程中出現效能問題有幾個原因:

  • 首先,大量的初始編譯可能直接影響應用程式的啟動時間。不僅這些編譯延遲了應用程式達到穩定狀態的時間(想像 Web 伺服器經歷一個初始階段後才能夠執行實際有用的工作),而且在準備階段中頻繁執行的方法可能對應用程式的穩定狀態的效能所起的作用也不大。如果 JIT 編譯會延遲啟動又不能顯著改善應用程式的長期效能,則執行這種編譯就非常浪費。雖然所有的現代 JVM 都執行調優來減輕啟動延遲,但是並非在所有情況下都能夠完全解決這個問題。

  • 有些應用程式完全不能忍受動態編譯帶來的延遲。如 GUI 介面之類互動式應用程式就是這樣的例子。在這種情況下,編譯活動可能對使用者使用造成不利影響,同時又不能顯著地改善應用程式的效能。

  • 最後,用於實時環境並具有嚴格的任務時限的應用程式可能無法忍受編譯的不確定性效能影響或動態編譯器本身的記憶體開銷。

因此,雖然 JIT 編譯技術已經能夠提供與靜態語言效能相當(甚至更好)的效能水平,但是動態編譯並不適合於某些應用程式。在這些情況下,Java 程式碼的提前(Ahead-of-time,AOT)編譯可能是合適的解決方案。

AOT提前編譯

動態類載入是動態 JIT 編譯器面臨的一個挑戰,也是 AOT 編譯的一個更重要的問題。只有在執行程式碼引用類的時候才載入該類。因為是在程式執行前進行 AOT 編譯的,所以編譯器無法預測載入了哪些類。就是說編譯器無法獲知任何靜態欄位的地址、任何物件的任何例項欄位的偏移量或任何呼叫的實際目標,甚至對直接呼叫(非虛呼叫)也是如此。在執行程式碼時,如果證明對任何這類資訊的預測是錯誤的,這意味著程式碼是錯誤的並且還犧牲了 Java 的一致性。

因為程式碼可以在任何環境中執行,所以類檔案可能與程式碼編譯時不同。例如,一個 JVM 例項可能從磁碟的某個特定位置載入類,而後面一個例項可能從不同的位置甚至網路載入該類。設想一個正在進行 bug 修復的開發環境:類檔案的內容可能隨不同的應用程式的執行而變化。此外,Java 程式碼可能在程式執行前根本不存在:比如 Java 反射服務通常在執行時生成新類來支援程式的行為。

缺少關於靜態、欄位、類和方法的資訊意味著嚴重限制了 Java 編譯器中優化框架的大部分功能。內聯可能是靜態或動態編譯器應用的最重要的優化,但是由於編譯器無法獲知呼叫的目標方法,因此無法再使用這種優化。

JIT vs JIT

  • JIT:吞吐量高,有執行時效能加成,可以跑得更快,並可以做到動態生成程式碼等,但是相對啟動速度較慢,並需要一定時間和呼叫頻率才能觸發 JIT 的分層機制

  • AOT:記憶體佔用低,啟動速度快,可以無需 runtime 執行,直接將 runtime 靜態連結至最終的程式中,但是無執行時效能加成,不能根據程式執行情況做進一步的優化

對比JIT和AOT

看了輪子哥的回答:

AOT是事先生成機器碼,其實就跟C++這樣的語言差不多。選擇這麼做通常都會意味著你損失了一個功能——譬如說

  1. C#的【虛擬函式也可以是模板函式】功能啦;

  2. 【用反射就地組合成新模板類(你有List<>,有int,程式碼裡面沒出現過List<int>,你也可以new出來,C++怎麼做都不行的)】功能啦;

  3. 【class Fuck<T>{public Fuck<Fuck<T>> Shit{get;set;}}(C++這麼幹編譯器會傻逼啊哈哈哈)】功能啦;

所有這些功能都要求你必須執行到那才產生機器碼的。

JIT還有一個好處就是做profiling based optimization方便。當然,這樣就使得執行的時候會稍微慢一點點,不過這一點點是人類不可察覺的。

AOT的缺陷

  • 應用安裝和系統升級之後的應用優化比較耗時(重新編譯,把程式程式碼轉換成機器語言)

  • 優化後的檔案會佔用額外的儲存空間(快取轉換結果)

總結來講:

  • 在開發期使用 JIT 編譯,可以縮短產品的開發週期。Flutter 最受歡迎的功能之一熱過載,正是基於此特 性。

  • 在釋出期使用 AOT,就不需要像 React Native 那樣在跨平臺 JavaScript 程式碼和原生 Android、iOS 程式碼之間建立低效的方法呼叫對映關係。

JIT和AOT共存

JIT與AOT各有千秋,兩者融合,比如大火的多端一體化 Flutter+Dart,其實不光做做客戶端咯,服務端應用有各自不同的執行特點,Dart能夠更好地適配。

Dart

Dart 是少數同時支援 JIT(Just In Time,即時編譯)和 AOT(Ahead of Time,執行前編譯)的語言之一。

Dart吸取了其它高階語言設計的精華,例如Smalltalk的Image技術、JVM的HotSpot和Dart編譯技術又師出同門。由Dart實現的語言容器,它可以在啟動速度、執行效能有不錯的表現。Dart提供了AoT、JIT的編譯方式,JIT擁有Kernel和AppJIT的執行模式

dart優勢

  • Dart在開發過程中使用JIT,因此每次改都不需要再編譯成位元組碼。節省了大量時間。

  • 在部署中使用AOT生成高效的ARM程式碼以保證高效的效能。

JIT 在執行時即時編譯,在開發週期中使用,可以動態下發和執行程式碼,開發測試效率高,但執行速度和執行效能則會因為執行時即時編譯受到影響。

所以說,Dart 具有執行速 度快、執行效能好的特點。

Android 

Android 7.0上,JIT 編譯器被再次使用,採用AOT/JIT 混合編譯的策略,特點是:

  1. 應用在安裝的時候dex不會再被編譯

  2. App執行時,dex檔案先通過解析器被直接執行,熱點函式會被識別並被JIT編譯後儲存在 jit code cache 中並生成profile檔案以記錄熱點函式的資訊。

  3. 手機進入 IDLE(空閒) 或者 Charging(充電) 狀態的時候,系統會掃描 App 目錄下的 profile 檔案並執行 AOT 過程進行編譯。

Dalvik,ART是Android的兩種執行環境,也可以叫做Android虛擬機器 JIT,AOT是Android虛擬機器採用的兩種不同的編譯策略

 

參考內容:

淺談JIT&AOT https://www.jianshu.com/p/ac079e7fc412

JIT(動態編譯)和AOT(靜態編譯)編譯技術比較 https://www.cnblogs.com/tinytiny/p/3200448.html

說一說Android的Dalvik,ART與JIT,AOT https://zhuanlan.zhihu.com/p/53723652

對比JIT和AOT,各自有什麼優點與缺點? - hez2010的回答 - 知乎 https://www.zhihu.com/question/23874627/answer/950484956

對比JIT和AOT,各自有什麼優點與缺點? - 圓胖腫的回答 - 知乎 https://www.zhihu.com/question/23874627/answer/889699901

 

 

轉載本站文章《JIT-動態編譯與AOT-靜態編譯:java/ java/ JavaScript/Dart亂談》,
請註明出處:https://www.zhoulujun.cn/html/theory/ComputerScienceTechnology/Compiling/2020_0714_8513.html

相關文章