電腦技術中,即時編譯(英語:Just-in-time compilation,縮寫為JIT;又譯及時編譯[1]即時編譯[2]),也稱為動態翻譯執行時編譯[3],是一種執行電腦代碼的方法,這種方法設計在程式執行過程中(在執行期)而不是在執行之前進行編譯[4]通常,這包括原始碼或更常見的位元組碼機械碼的轉換,然後直接執行。實現 JIT 編譯器的系統通常會不斷地分析正在執行的代碼,並確定代碼中可被即時編譯加速的部分,在這些部分中,由編譯或重新編譯帶來的效能提高將超過編譯該代碼的開銷。

不同類型的即時編譯實現

JIT編譯是兩種傳統的機械碼翻譯方法——提前編譯英語ahead-of-time compilation(AOT)和直譯器——的結合,它結合了兩者的優點和缺點。[4]大致來說,JIT編譯,以直譯器的開銷以及編譯和連結(解釋之外)的開銷,結合了編譯代碼的速度與解釋的靈活性。JIT編譯是動態編譯的一種形式,允許自適應最佳化英語adaptive optimization,比如動態重編譯和特定於微架構的加速[nb 1][5]——因此,在理論上,JIT編譯比靜態編譯能夠產生更快的執行速度。解釋和JIT編譯特別適合於動態程式語言,因為執行時系統可以處理後期繫結英語Late binding的資料類型並實施安全保證。

應用

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

JIT編譯的一個常見實現是首先進行AOT編譯,把原始碼編譯成位元組碼(虛擬機器代碼),稱為位元組碼編譯,然後將JIT編譯為機械碼(動態編譯),而不是解釋位元組碼。與解釋相比,這提高了執行時效能,但代價是編譯造成的延遲。與直譯器一樣,JIT編譯器不斷地進行翻譯,但是對編譯後的代碼進行快取可以最大限度地減少在給定執行期間將來執行相同代碼的延遲。

概述

在位元組碼編譯的系統中,原始碼被轉換為稱為位元組碼的中間表示形式。位元組碼不是任何特定電腦的機械碼,可以在電腦架構之間移植。然後可以在虛擬機器上解釋或執行位元組碼。JIT編譯器在許多部分(或全部、很少)讀取位元組碼,並將它們動態編譯成機械碼,以便程式能夠更快地執行。這可以針對每個檔案、每個函數甚至任何任意代碼片段進行編譯; 代碼可以在即將執行時進行編譯(因此稱為「即時」),然後快取並在以後重用,無需重新編譯。

相比之下,傳統的解釋型虛擬機器只解釋位元組碼,通常效能要低得多。有些直譯器甚至不需要首先編譯成位元組碼就可以解釋原始碼,但效能更差。靜態編譯的代碼本地代碼在部署之前編譯。動態編譯環境是在執行期間可以使用編譯器的環境。 使用JIT技術的一個共同目標是達到或超過靜態編譯的效能,同時保持位元組碼解釋的優勢:解析原始原始碼和執行基本最佳化的許多「繁重工作」通常是在編譯時處理的,在部署之前:從位元組碼編譯到機械碼要比從原始碼編譯快得多。與本地代碼不同,部署的位元組碼是可移植的。由於執行時可以控制編譯,比如解釋位元組碼,所以它可以在安全的沙箱中執行。從位元組碼到機械碼的編譯器更容易編寫,因為可攜式位元組碼編譯器已經完成了大部分工作。

JIT代碼通常比直譯器效能更好。另外,在某些情況下,它的效能可以比靜態編譯更好,因為許多最佳化只在執行時可行:[6][7]

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

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

啟動延遲和最佳化

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

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

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

微軟本地鏡像生成器英語Native Image Generator(Ngen)是另一種減少初始延遲的方法。[14]Ngen將通用中間語言映像英語Common Intermediate Language中的位元組碼預編譯成機器本機代碼。因此,不需要執行時編譯。Visual Studio 2005附帶的.NET Framework 2.0在安裝之後立即在所有微軟庫dll上執行Ngen。預JIT提供了一種提高啟動時間的方法。但是,它生成的代碼質素可能不如JIT生成的代碼質素好,原因與靜態編譯的代碼(沒有按組態最佳化英語profile-guided optimization)在極端情況下不如JIT編譯的代碼的原因相同:缺乏分析數據來驅動,例如,行內快取。[15]

還有一些Java實現將AOT編譯器與JIT編譯器(Excelsior JET)或直譯器(GNU Compiler for Java)結合起來。

歷史

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

Smalltalk(1983年)開創了JIT編譯的新領域。例如,按需翻譯為機械碼,快取結果以供以後使用。當主記憶體不足時,系統會刪除部分代碼,並在需要時重新生成。[4][21]Sun的Self語言廣泛地改進了這些技術,一度是世界上速度最快的Smalltalk系統;運用完全物件導向的語言實現了高達最佳化C語言一半的速度。[22]

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

HP的專案Dynamo[24]是一個實驗性的JIT編譯器,其位元組碼格式和機械碼格式是相同的;該系統將PA-6000機械碼轉換為PA-8000機械碼。與直覺相反,這導致了速度的提高,在某些情況下是30%,因為這樣做允許在機械碼級別進行最佳化,例如,行內代碼以更好地使用快取,最佳化對動態庫的呼叫,以及許多其他常規編譯器無法嘗試的執行時最佳化。[25][26]

2019年3月30日,PHP宣佈JIT將於2021年加入PHP 8[27][28]

安全

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

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

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

參見

註釋

參考文獻

外部連結

Wikiwand in your browser!

Seamless Wikipedia browsing. On steroids.

Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.

Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.