即時編譯

<image>

在電腦技術中，即時編譯（Just-in-time compilation，縮寫為JIT；又譯及時編譯、實時編譯），也稱為動態翻譯或運行時編譯，是一種執行電腦代碼的方法，這種方法設計在程序執行過程中（在執行期）而不是在執行之前進行編譯。通常，這包括原始碼或更常見的字節碼到機器碼的轉換，然後直接執行。實現 JIT 編譯器的系統通常會不斷地分析正在執行的代碼，並確定代碼中可被即時編譯加速的部分，在這些部分中，由編譯或重新編譯帶來的性能提高將超過編譯該代碼的開銷。

JIT編譯是兩種傳統的機器代碼翻譯方法——提前編譯（AOT）和直譯器——的結合，它結合了兩者的優點和缺點。大致來說，JIT編譯，以解釋器的開銷以及編譯和連結（解釋之外）的開銷，結合了編譯代碼的速度與解釋的靈活性。JIT編譯是動態編譯的一種形式，允許自適應優化，比如動態重編譯和特定於微架構的加速——因此，在理論上，JIT編譯比靜態編譯能夠產生更快的執行速度。解釋和JIT編譯特別適合於動態程式語言，因為運行時系統可以處理後期綁定的數據類型並實施安全保證。

## 應用

JIT編譯可以應用於某些程序，也可以用於某些能力，特別是動態能力，如正則表達式。例如，一個文本編輯器可以把運行時提供的正則表達式編譯成機器碼，從而更快地進行匹配——這不能提前完成，因為pattern只在運行時提供。一些現代的運行時環境依賴JIT編譯來實現高速代碼執行，包括大多數Java實現，以及微軟的.NET框架。類似地，許多正則表達式庫都具有對正則表達式進行JIT編譯的功能，可以編譯成字節碼，也可以編譯成機器碼。JIT編譯也用於一些模擬器中，以便將機器代碼從一個CPU體系結構轉換到另一個CPU體系結構。

JIT編譯的一個常見實現是首先進行AOT編譯，把原始碼編譯成字節碼（虛擬機代碼），稱為字節碼編譯，然後將JIT編譯為機器碼（動態編譯），而不是解釋字節碼。與解釋相比，這提高了運行時性能，但代價是編譯造成的延遲。與解釋器一樣，JIT編譯器不斷地進行翻譯，但是對編譯後的代碼進行緩存可以最大限度地減少在給定運行期間將來執行相同代碼的延遲。

## 概述

在字節碼編譯的系統中，原始碼被轉換為稱為字節碼的中間表示形式。字節碼不是任何特定電腦的機器代碼，可以在計算機體系結構之間移植。然後可以在虛擬機上解釋或運行字節碼。JIT編譯器在許多部分（或全部、很少）讀取字節碼，並將它們動態編譯成機器代碼，以便程序能夠更快地運行。這可以針對每個文件、每個函數甚至任何任意代碼片段進行編譯; 代碼可以在即將執行時進行編譯（因此稱為「即時」），然後緩存並在以後重用，無需重新編譯。

相比之下，傳統的解釋型虛擬機只解釋字節碼，通常性能要低得多。有些解釋器甚至不需要首先編譯成字節碼就可以解釋原始碼，但性能更差。靜態編譯的代碼或本地代碼在部署之前編譯。動態編譯環境是在執行期間可以使用編譯器的環境。

使用JIT技術的一個共同目標是達到或超過靜態編譯的性能，同時保持字節碼解釋的優勢：解析原始原始碼和執行基本優化的許多「繁重工作」通常是在編譯時處理的，在部署之前：從字節碼編譯到機器碼要比從原始碼編譯快得多。與本地代碼不同，部署的字節碼是可移植的。由於運行時可以控制編譯，比如解釋字節碼，所以它可以在安全的沙箱中運行。從字節碼到機器碼的編譯器更容易編寫，因為可攜式字節碼編譯器已經完成了大部分工作。

JIT代碼通常比解釋器性能更好。另外，在某些情況下，它的性能可以比靜態編譯更好，因為許多優化只在運行時可行：

- 編譯可以針對目標CPU和應用程式運行的作業系統模型進行優化。例如，JIT可以在檢測到CPU支持SSE2矢量CPU指令時選擇它們。要使用靜態編譯器獲得這種優化級別的特殊性，必須為每個預期的平台/體系結構編譯一個二進位文件，或者在一個二進位文件中包含多個版本的部分代碼。
- 該系統能夠收集關於程序在其所在環境中實際運行情況的統計資訊，並且能夠重新排列和重新編譯以獲得最佳性能。但一些靜態編譯器也可以將概要資訊作為輸入。
- 該系統可以進行全局代碼優化（例如內聯庫函數），同時不失去動態連結的優點，也不會失去靜態編譯器和連結器固有的開銷。具體來說，在進行全局內聯替換時，靜態編譯過程可能需要運行時檢查，並確保如果對象的實際類重寫了內聯方法，就會發生虛擬調用，並且對數組訪問的邊界條件檢查可能需要在循環中處理。在許多情況下，使用即時編譯，這種處理可以從循環中移出，通常會大大提高速度。
- 儘管使用靜態編譯的垃圾收集語言可以做到這一點，但字節碼系統可以更容易地重新排列執行的代碼，以獲得更好的緩存利用率。

由於JIT必須在運行時呈現和執行本地二進位映像，因此真正的機器代碼JIT需要允許在運行時執行數據的平台，這使得在基於哈佛結構的機器上使用這種JIT成為不可能的事情——對於某些作業系統和虛擬機也是如此。然而，一種特殊類型的「JIT」可能並不針對物理機器的CPU體系結構，而是一種優化的VM字節碼，在這種情況下，對原始機器代碼的限制占了上風，特別是在字節碼的VM最終將JIT用於本機代碼的情況下。

## 啟動延遲和優化

由於加載和編譯字節碼所需的時間，JIT在應用程式的初始執行中會導致輕微到明顯的延遲。有時這種延遲被稱為「啟動時間延遲」或「預熱時間」。一般來說，JIT執行的優化越多，生成的代碼就越好，但是初始延遲也會增加。因此，JIT編譯器必須在編譯時間和希望生成的代碼質量之間進行權衡。除了JIT編譯之外，IO綁定操作也會增加啟動時間：例如，JVM的「rt.jar」類數據文件為40 MB，JVM必須在這個巨大的上下文文件中尋找大量數據。

Sun的HotSpot Java虛擬機使用的一種可能的優化方法是將解釋和JIT編譯結合起來。應用程式代碼最初是被解釋的，但JVM監視哪些字節碼序列經常被執行，並將它們轉換為機器代碼，以便在硬體上直接執行。對於只執行幾次的字節碼，這節省了編譯時間並減少了初始延遲；對於頻繁執行的字節碼，JIT編譯用於在緩慢解釋的初始階段之後以高速運行。此外，由於程序花費大量時間執行的其實只是一小部分代碼，因此減少的編譯時間非常重要。最後，在初始代碼解釋期間，可以在編譯之前收集執行統計資訊，這有助於執行更好的優化。

正確的權衡可以根據具體情況而變化。例如，Sun的Java虛擬機有兩種主要模式: 客戶機和伺服器。在客戶端模式下，執行最小程度的編譯和優化，以減少啟動時間。在伺服器模式下，將執行大量的編譯和優化，以犧牲啟動時間來最大限度地提高應用程式運行時的性能。其他Java即時編譯器使用一個方法執行次數的運行時度量，結合方法的字節碼大小作為一種啟發式方法來決定何時編譯。還有的使用執行的次數與檢測循環相結合。一般來說，在短期運行的應用程式中準確預測要優化的方法要比在長期運行的應用程式中準確得多。

微軟的本地鏡像生成器（Ngen）是另一種減少初始延遲的方法。Ngen將通用中間語言映像中的字節碼預編譯成機器本機代碼。因此，不需要運行時編譯。Visual Studio 2005附帶的.NET Framework 2.0在安裝之後立即在所有微軟庫dll上運行Ngen。預JIT提供了一種提高啟動時間的方法。但是，它生成的代碼質量可能不如JIT生成的代碼質量好，原因與靜態編譯的代碼（沒有按配置優化）在極端情況下不如JIT編譯的代碼的原因相同：缺乏分析數據來驅動，例如，內聯緩存。

還有一些Java實現將AOT編譯器與JIT編譯器（Excelsior JET）或解釋器（GNU Compiler for Java）結合起來。

## 歷史

最早發布的JIT編譯器通常歸功於約翰·麥卡錫在1960年對LISP的研究。在他的重要論文《符號表達式的遞歸函數及其在機器上的計算》（Recursive functions of symbolic expressions and their computation by machine, Part I）第一部分中，他提到了在運行時被轉換的函數，因此不需要保存編譯器輸出來打孔卡（雖然更準確的說法是「編譯並執行系統」）。另一個早期的應用來自肯·湯普遜，他在文本編輯器QED的正則表達式模式匹配中使用了JIT。為了提高速度，Thompson在相容分時系統上通過JIT到IBM 7090代碼實現了正則表達式匹配。1970年，Mitchell首創了一種有影響力的從解釋中獲取編譯代碼的技術，他在實驗語言LC²中實現了這種技術。

Smalltalk（1983年）開創了JIT編譯的新領域。例如，按需翻譯為機器代碼，緩存結果以供以後使用。當記憶體不足時，系統會刪除部分代碼，並在需要時重新生成。Sun的Self語言廣泛地改進了這些技術，一度是世界上速度最快的Smalltalk系統；運用完全面向對象的語言實現了高達優化C語言一半的速度。

Self被Sun拋棄了，但是研究轉向了Java語言。「即時編譯」這個術語是從製造術語「及時」中借來的，並由Java普及，James Gosling從1993年開始使用這個術語。目前，大多數Java虛擬機的實現都使用JIT技術，因為HotSpot建立在這個研究基礎之上，而且使用廣泛。

HP的項目Dynamo是一個實驗性的JIT編譯器，其字節碼格式和機器代碼格式是相同的；該系統將PA-6000機器代碼轉換為PA-8000機器代碼。與直覺相反，這導致了速度的提高，在某些情況下是30%，因為這樣做允許在機器代碼級別進行優化，例如，內聯代碼以更好地使用緩存，優化對動態庫的調用，以及許多其他常規編譯器無法嘗試的運行時優化。

2019年3月30日，PHP宣布JIT將於2021年加入PHP 8。

2024年10月，CPython引進了一個實驗性的JIT編譯器。

## 安全

JIT編譯從根本上使用可執行數據，因此帶來了安全挑戰和可能的漏洞。

JIT編譯的實現包括將原始碼或字節碼編譯成機器碼並執行它。這通常是直接在記憶體中完成的——JIT編譯器將機器代碼直接輸出到記憶體中並立即執行，而不是像通常的提前編譯那樣將其輸出到磁碟，然後作為單獨的程序調用代碼。在現代的體系結構中，由於可執行空間保護，這會遇到一個問題——無法在任意記憶體裡執行程式。在任意記憶體裡執行程式存在潛在的安全漏洞。因此，必須將記憶體標記為可執行；出於安全原因，應在代碼寫入記憶體並標記為只讀之後執行，因為可寫/可執行記憶體是一個安全漏洞（參見W^X）。例如 Firefox 中的 JavaScript JIT 編譯器在 Firefox 46 版本中引入了這種保護。

JIT噴射是一種利用漏洞利用的技術，它使用JIT編譯進行堆噴射——生成的記憶體然後是可執行的，如果執行可以移動到堆積中，這就允許利用。

## 外部連結

- 自由在線電腦詞典的詞條
- Mozilla Nanojit — 一個小型的可以產生機器碼的跨平台C++庫。它被用作Mozilla Tamarin和SpiderMonkey Javascript引擎的JIT。
- OVPsim，一種嵌入式核心JIT工具，它將ARM、MIPS和其他ISA指令轉換為x86進行執行/模擬
- 使用「VTune性能分析器」評測運行時生成和解釋的代碼