JIT(Just In Time簡稱JIT)是.Net邊運行邊編譯的一種機制,這種機制的命名來源於豐田汽車在20世紀60年代實行的一種生產方式,中文譯為“准時制”。
.Net 的JIT編譯器在設計初衷和運行方式來上講,都與豐田汽車的這種“准時生產”思想體系有著很大的相似之處,所以讓我們先來透過“准時生產”方式來理解.Net的JIT機制吧。
“准時生產”的基本思想可概括為“在需要的時候,按需要的量生產所需的產品”,這正是.Net JIT編譯器的設計初衷,即在需要的時候編譯需要的代碼。
第一節.Me JIT
以C#為例,在C#代碼運行前,一般會經過兩次編譯,第一階段是C#代碼向MSIL的編譯,第二階段是IL向本地代碼的編譯。第一階段的編譯成果是生成托管模塊,第二階段的編譯成果是生成本地代碼以供運行,從這裡各位同學可以看出,第一階段生成的MSIL是不能直接運行的。
這裡先要解釋一下什麼是MSIL和托管模塊。
MSIL:
MSIL 全稱為Microsoft Intermediate Language,中文譯為“微軟中間語言”,它是一種介於高級語言和匯編語言之間的偽匯編語言(姑且這麼叫,各位有不同意見的同學不必激動)。當用戶編譯運行一個.NET程序時,高級語言編譯器會將源代碼翻譯成一組可以獨立於CPU的指令。
可以看出IL 包括用於加載(ldstr )、存儲(壓棧、彈棧)和初始化對象(locals)以及調用對象方法(call)的指令,還包括用於算術和邏輯運算、控制流、直接內存訪問、異常處理和其他操作的指令。
C#代碼:
string str_test = "test";
System.String Str_test = "test";
對應IL碼:
// 代碼大小 14 (0xe)
.maxstack 1
.locals init ([0] string str_test,
[1] string Str_test)
IL_0000: nop
IL_0001: ldstr "test"
IL_0006: stloc.0
IL_0007: ldstr "test"
IL_000c: stloc.1
IL_000d: ret
托管模塊:
托管模塊(managed module)是一個標准32位或64位Microsoft Windows可移植可執行體(PE32或PE32+)文件,托管模塊需要CLR才能執行,它包含了上面介紹的IL代碼,還包含元數據、PE頭、CLR頭幾部分。
元數據(metadata)可以理解為一個HashTable,Table中映射了內置類型和成員以及引用的類型和成員,這些類型與成員供IL使用,所以元數據總是需要關聯對應的IL代碼,編譯器也是同時生成元數據與IL,以保證自描述的同步。
PE頭(Portable Executable,中文譯為可移植的可執行的)包括了PE32與PE32+,標示了托管模塊的運行環境以及JIT優化本地代碼時所要用到的信息,這在後面會講到。
CLR頭主要包括方法的入口地址標記,以及資源、強命名等信息,這些信息是GAC重要的參數依據。
下圖可以表示出JIT的介入時機:
圖1 JIT工作時機
JIT是運行時的一個重要職責模塊,它將IL轉換為本地CPU指令,從上圖可以看出,也許你不敢相信,即時編譯這個過程是在運行時發生的,這會不會對性能產生影響呢?事實上答案是雖然是肯定的,但這種開銷物有所值:
JIT所造成的性能開銷並不顯著。
JIT遵循計算機體系理論中兩個經典理論:局部性原理與8020原則。局部性原理指出,程序總是趨向於使用最近使用過的數據和指令,這包括空間的和時間的,將局部性原理引申可以得出,程序總是趨向於使用最近使用過的數據和指令,以及這些正在使用的數據和指令臨近的數據和指令(憑印象寫的,但不曲解原意);而8020原則指出,系統大多數時間總是花費80%的時間去執行那20%的代碼。 根據這兩個原則,JIT在運行時會實時的向前、後優化代碼,這樣的工作只有在運行時才可以做到。
JIT只編譯需要的那一段代碼,而不是全部,這樣節約了不必要的內存開銷。
JIT會根據運行時環境,即時的優化IL代碼,即同樣的IL代碼運行在不同CPU上,JIT編譯出的本地代碼是不同的,這些不同代碼面向自己的CPU做出了優化。
JIT會對代碼的運行情況進行檢測,並對那些特殊的代碼經行重新編譯,在運行過程中不斷優化。
實際上JIT的優點還不止如此,它對內聯、策略引擎(.Net Discovery 系列之四--深入理解.Net垃圾收集機制(下) 中包含對策略引擎的描述)、CLR反饋、代碼回收(非垃圾回收,這在第二節中會有介紹)等方面都會有不可磨滅的貢獻。
必須指出的是JIT在第一次編譯IL後,會修改對應方法相應的內存地址入口(繞口啊~~),下一次需要執行這個方法時,CLR會直接訪問對應的內存地址,而不會經過JIT了。
第二節.編譯與執行
在上一節中我們討論了與JIT相關的一些元素和JIT的優勢,這一節將為大家重點介紹JIT在編譯方面的原理。
C#等高級語言必須被編譯為IL才可被執行,IL在執行前必須被便以為本地代碼才可運行,這裡有兩種方法可以獲得本地代碼,JIT方式和Native Image Generator方式,本節主要討論JIT方式。
必須指出的是JIT在第一次編譯IL後,會修改對應方法相應的內存地址入口,下一次需要執行這個方法時,CLR會直接訪問對應的內存地址,而不會經過JIT了,這樣無疑加快了程序運行的速度,這是怎樣的一個過程呢?
以Load()方法為例,假如Load()方法中調用了兩次同類型中的方法:
Void Load()
{
A.a1("First");
A.a1("Second");
}
static class A
{
Public void a1(string str){}
Public void a2(string str){}
Public void a3(string str){}
}
運行時,操作系統會根據托管模塊中各種頭信息,裝載相應的運行時框架,Load()被加載,由於是第一次加載,這會觸發對Load()的即時編譯,JIT 會檢測Load()中引用的所有類型,並結合元數據遍歷這些類型中定義的所有方法實現,並用一個特殊的HashTable(僅用於理解)儲存這些類型方法與其對應的入口地址(在未被JIT前,這個入口地址為一個預編譯代理(PreJitStub),這個代理負責觸發JIT編譯),根據這些地址,就可以找到對應的方法實現。