即時編譯

在電腦技術中，**即時編譯**（英語：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 代碼通常比直譯器效能更好。另外，在某些情況下，它的效能可以比靜態編譯更好，因為許多最佳化只在執行時可行：

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

由於 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。

## 安全

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

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

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

## 參見

* 二進位翻譯
* HotSpot
* 通用語言運行庫
* Crusoe，一種微處理器，基本上執行從 x86 代碼到微處理器內部微碼的即時編譯
* GNU lightning — 一個在執行時生成組合語言代碼的庫
* LLVM
* 自修改代碼
* 跟蹤即時編譯
* 動態編譯

## 注釋

## 參考文獻

### 參照

### 來源

## 外部連結

* 自由線上電腦詞典的詞條
* Mozilla Nanojit （頁面存檔備份，存於網際網路檔案館） — 一個小型的可以產生機器碼的跨平台 C++ 庫。它被用作 Mozilla Tamarin 和 SpiderMonkey Javascript 引擎的 JIT。
* OVPsim，一種嵌入式核心 JIT 工具，它將 ARM、MIPS 和其他 ISA 指令轉換為 x86 進行執行 / 類比
* 使用「VTune 效能剖析器」評測執行時生成和解釋的代碼（頁面存檔備份，存於網際網路檔案館）