天天看點

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

    接上文

    在初始化時,HashTable中各個方法指向的并不是對應的記憶體入口位址,而是一個JIT預編譯代理,這個函數負責将方法編譯為本地代碼。<b>注意,這裡JIT還沒有進行編譯,隻是建立了方法表</b>!

    下表(表1)為首次加載調用時HashTable的情況:

                     表1 方法表示意

方法槽

方法描述

a1()

PreJitStub

a2()

a3()

    好了有了這個HashTable後,JIT開始編譯第一個被調用的方法A.a1("First"),這是由一個JIT内部函數來完成的(上面提到的),遺憾的事,目前還沒有發現介紹這個函數的相關資料,有些書中稱它為“JIT編譯者”,那本文也這麼稱呼它吧。

    下圖為首次調用方法時的示意圖:

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

圖2 觸發JIT編譯

    JIT借助中繼資料和IL生成被調用方法的本地代碼後,會将這些代碼緩存在動态記憶體中,然後修改HashTable中對應方法的入口位址,将其修改為本地代碼的記憶體片位址(如表2所示),并将這個位址傳回給CLR經行執行,A.a1("First")執行完畢,代碼繼續運作。

    運作至A.a1("Second ")時,會直接執行A.a1()方法的記憶體代碼,不會進行再次編譯,表2 為再次加載時HashTable的情況。

              表2 方法表變化

XXXXXXXXX記憶體位址

    再次加載流程示意圖:

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

圖3 未觸發JIT編譯

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

圖4 方法表、方法描述、預編譯代理關系

    圖2中所示的MS核心引擎指的是一個叫做MSCorEE的DLL,即Microsoft .NET Runtime Execution Engine,它是一個橋接DLL,連同mscorwks.dll主要完成以下工作:

查找程式集中包含的對應類型清單,并調用中繼資料周遊出包含的方法。

結合中繼資料獲得這個方法的IL。

配置設定記憶體。

編譯IL為本地代碼,并儲存在第3步所配置設定的記憶體中。

将類型表(就是指上文中提到的HashTable)中方法位址修改為第3步所配置設定的記憶體位址。

跳轉至本地代碼中執行。

    是以随着程式的運作時間增加,越來越多的方法的IL被編譯為本地代碼,JIT的調用次數也會不斷減少。

      下面借助WinDbg來證明以上的說法,示例中的源程式可以到這裡下載下傳到:

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

namespace JITTester

{

public partial class Form1 : Form

public Form1()

InitializeComponent();

}

private void Form1_Load(object sender, EventArgs e)

private void GO_Click(object sender, EventArgs e)

new A().a1();

lb_msg.Text = "調用完畢!";

class A

public void a1() { }

public C a2 = new C();

class B

public void b1() { }

public void b2() { }

class C

public void c1() { }

public void c2() { }

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

      代碼中定義了3個類,分别為A、B、C,在“GO”按鈕按下後,将調用類型A中的a1()方法,而Form1_Load 中什麼也不做,目的是程式運作後,在空載的情況下檢視方法描述對應位址入口的情況。

    好,第一步運作JITTester.exe程式,并打開WinDbg附加這個程序

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

圖 5 附件程序

   第二步,附加程序成功後,在WinDbg中加載SOS.dll

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

圖6 加載SOS.dll

    第三步,使用name2ee指令周遊所有已加載子產品,name2ee格式為name2ee *! [程式集].[類型]

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

圖7 檢視類型資訊

    回車後注意高亮區域的資訊:

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

圖8 JIT前A類型的資訊

    高亮區域顯示的是“&lt;not loaded yet&gt;”,這說明雖然運作和程式,但未點選按鈕時,A類型未被JIT,因為它還沒有入口位址。這一點展現了即時、按需編譯的思想。

   同樣,!name2ee *!JITTester.B和!name2ee *!JITTester.C指令會得到同樣的結果。

    好,現在做第4步操作,Detach Debuggee程序,并回到程式中點選“GO”按鈕

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

圖9 點選按鈕

    第五步 重新附加程序(參考第一步),這時程式已經調用了new A().a1()方法,并重新執行指令!name2ee *!JITTester.A ,注意高亮部分

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

圖10 JIT後A類型的資訊

    和圖8中的資訊比較,圖10中的方法表位址已經變為JIT後的記憶體位址,這時圖4中的Stub槽将被一條強制跳轉語句替換,跳轉目标與該位址有關。這一點說明JIT在大多情況下,隻編譯一次代碼。

    同樣指令檢視B類型:

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

圖11 JIT後B類型的資訊

    該類型未被調用,是以還未被JIT。

    C類型:

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

圖12 JIT後C類型的資訊

    由于執行個體化A類型時和C類型相關,是以C類型已經JIT了。

<b>    第三節.</b><b>Native Image Generator</b>

    Native Image Generator中文譯為本地代碼生成器,我更習慣叫它“本地映像”,因為通過工具NGen.exe生成的本地代碼是無法部分載入的,這意味着作業系統會加載整個程式集檔案。

    上一節中提到過,有兩種方法可以獲得本地代碼,JIT方式和Native Image Generator方式,JIT方式是在運作時動态編譯需要的代碼,而NGen.exe會建立托管程式集的本機映像,并且将該映像安裝到GAC中,運作該程式集時,就會自動使用該本機映像而不是JIT它們。

這聽起來似乎很美妙,但是你必須做好以下準備:

當FrameWork版本、CPU類型、作業系統版本發生變化時,.Net會恢複JIT機制。

NGen.exe工具并不能避免釋出IL,事實上,即使使用NGen.exe工具,CLR依然會使用到中繼資料和IL。<b> </b>

忽略了局部性原理(上一節中提到的),系統會加載整個映像檔案到記憶體中,并很可能重定位檔案,修正記憶體位址引用。<b> </b>

NGen.exe生成的代碼無法在運作時進行優化,無法直接通路靜态資源,也無法在應用程式域之間共享程式集。<b> </b>

    此外,JIT不但有編譯的本事,還會根據記憶體資源情況換出使用率低的代碼,節省資源,這對于一些基于.Net平台的電子産品是很重要的。<b> </b>

    是以,除非你已十厘清楚程式性能是由于首次編譯造成的性能問題,否則盡量不要人工生成本地代碼。<b> </b>

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

    “.Net Discovery 系列”推薦:

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

繼續閱讀