天天看點

.Net Discovery 系列之五--深入淺出.Net實時編譯機制(上)

    歡迎閱讀“.Net Discovery 系列”文章,本文将分上、下兩部分為大家講解.Net JIT方面的知識,敬請雅正。

    JIT(Just In Time簡稱JIT)是.Net邊運作邊編譯的一種機制,這種機制的命名來源于豐田汽車在20世紀60年代實行的一種生産方式,中文譯為“準時制”。

    .Net 的JIT編譯器在設計初衷和運作方式來上講,都與豐田汽車的這種“準時生産”思想體系有着很大的相似之處,是以讓我們先來透過“準時生産”方式來了解.Net的JIT機制吧。

    “準時生産”的基本思想可概括為“在需要的時候,按需要的量生産所需的産品”,這正是.Net JIT編譯器的設計初衷,即在需要的時候編譯需要的代碼。

<b>    第一節.</b><b>Me JIT</b>

    以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碼:   

.Net Discovery 系列之五--深入淺出.Net實時編譯機制(上)

// 代碼大小 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

.Net Discovery 系列之五--深入淺出.Net實時編譯機制(上)

    托管子產品:

    托管子產品(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的介入時機:

.Net Discovery 系列之五--深入淺出.Net實時編譯機制(上)

圖1 JIT工作時機

    JIT是運作時的一個重要職責子產品,它将IL轉換為本地CPU指令,從上圖可以看出,也許你不敢相信,即時編譯這個過程是在運作時發生的,這會不會對性能産生影響呢?事實上答案是雖然是肯定的,但這種開銷物有所值:

JIT所造成的性能開銷并不顯著。

JIT遵循計算機體系理論中兩個經典理論:局部性原理與8020原則。局部性原理指出,程式總是趨向于使用最近使用過的資料和指令,這包括空間的和時間的,将局部性原理引申可以得出,程式總是趨向于使用最近使用過的資料和指令,以及這些正在使用的資料和指令臨近的資料和指令(憑印象寫的,但不曲解原意);而8020原則指出,系統大多數時間總是花費80%的時間去執行那20%的代碼。   根據這兩個原則,JIT在運作時會實時的向前、後優化代碼,這樣的工作隻有在運作時才可以做到。

JIT隻編譯需要的那一段代碼,而不是全部,這樣節約了不必要的記憶體開銷。

JIT會根據運作時環境,即時的優化IL代碼,即同樣的IL代碼運作在不同CPU上,JIT編譯出的本地代碼是不同的,這些不同代碼面向自己的CPU做出了優化。

JIT會對代碼的運作情況進行檢測,并對那些特殊的代碼經行重新編譯,在運作過程中不斷優化。

    必須指出的是JIT在第一次編譯IL後,會修改對應方法相應的記憶體位址入口(繞口啊~~),下一次需要執行這個方法時,CLR會直接通路對應的記憶體位址,而不會經過JIT了。

<b>   第二節.編譯與執行</b>

    在上一節中我們讨論了與JIT相關的一些元素和JIT的優勢,這一節将為大家重點介紹JIT在編譯方面的原理。

    C#等進階語言必須被編譯為IL才可被執行,IL在執行前必須被便以為本地代碼才可運作,這裡有兩種方法可以獲得本地代碼,JIT方式和Native Image Generator方式,本節主要讨論JIT方式。

    必須指出的是JIT在第一次編譯IL後,會修改對應方法相應的記憶體位址入口,下一次需要執行這個方法時,CLR會直接通路對應的記憶體位址,而不會經過JIT了,這樣無疑加快了程式運作的速度,這是怎樣的一個過程呢?

    以Load()方法為例,假如Load()方法中調用了兩次同類型中的方法:

.Net Discovery 系列之五--深入淺出.Net實時編譯機制(上)

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){}

.Net Discovery 系列之五--深入淺出.Net實時編譯機制(上)

    運作時,作業系統會根據托管子產品中各種頭資訊,裝載相應的運作時架構,Load()被加載,由于是第一次加載,這會觸發對Load()的即時編譯,JIT會檢測Load()中引用的所有類型,并結合中繼資料周遊這些類型中定義的所有方法實作,并用一個特殊的HashTable(僅用于了解)儲存這些類型方法與其對應的入口位址(在未被JIT前,這個入口位址為一個預編譯代理(PreJitStub),這個代理負責觸發JIT編譯),根據這些位址,就可以找到對應的方法實作。

    未完待續

    我是李鳴(Aicken) 請您繼續關注我的下一篇文章。

    “.Net Discovery 系列”推薦:

<b>本文轉自Aicken(李鳴)部落格園部落格,原文連結:http://www.cnblogs.com/isline/archive/2009/12/22/1629831.html,如需轉載請自行聯系原作者</b>

繼續閱讀