天天看點

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

  上一篇文章中Aicken為大家介紹了.Net平台的垃圾回收機制與其對性能的影響,這一篇中将繼續為大家介紹.Net平台的另一批黑馬—JIT。有關JIT的機制分析

  ● 機制分析以C#為例,在C#代碼運作前,一般會經過兩次編譯,第一階段是C#代碼向MSIL的編譯,第二階段是IL向本地代碼的編譯。第一階段的編譯成果是生成托管子產品,第二階段的編譯成果是生成本地代碼以供運作,從這裡各位同學可以看出,第一階段生成的MSIL是不能直接運作的。必須指出的是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編譯),根據這些位址,就可以找到對應的方法實作。在初始化時,HashTable中各個方法指向的并不是對應的記憶體入口位址,而是一個JIT預編譯代理,這個函數負責将方法編譯為本地代碼。注意,這裡JIT還沒有進行編譯,隻是建立了方法表!

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

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

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

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

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

  3.配置設定記憶體。

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

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

  6.跳轉至本地代碼中執行。是以随着程式的運作時間增加,越來越多的方法的IL被編譯為本地代碼,JIT的調用次數也會不斷減少。下面借助WinDbg來證明以上的說法,加載WinDbg的過程略。以下測試源代碼可以從這裡下載下傳http://files.cnblogs.com/isline/IsLine.JITTester.rar

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

使用name2ee指令周遊所有已加載子產品,如下圖:

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

圖3 檢視類型資訊

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

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

圖4 JIT前A類型的資訊

  高亮區域顯示的是,這說明雖然運作和程式,但未點選按鈕時,A類型未被JIT,因為它還沒有入口位址。這一點展現了即時、按需編譯的思想。同樣,!name2ee *!JITTester.B和!name2ee *!JITTester.C指令會得到同樣的結果。好,現在繼續,Detach Debuggee程序,并回到程式中點選GO按鈕

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

圖5 點選按鈕

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

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

圖6 JIT後A類型的資訊

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

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

圖7 JIT後B類型的資訊

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

一起談.NET技術,.Net Discovery系列之-深入了解平台機制與性能影響 (中)

圖8 JIT後C類型的資訊

  由于執行個體化A類型時和C類型相關,是以C類型已經JIT了。這就是一個類型被JIT的全部過程。

  ● 性能影響分析通過以上的分析,大家已經能夠了解,即時編譯這個過程是在運作時發生的,這會不會對性能産生影響呢?事實上答案是雖然是肯定的,但這種開銷物有所值,并且如上所說的,JIT在第一次編譯IL後,會修改對應方法相應的記憶體位址入口(繞口啊~~),下一次需要執行這個方法時,CLR會直接通路對應的記憶體位址,而不會經過JIT了。

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

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

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

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

  5.JIT會對代碼的運作情況進行檢測,并對那些特殊的代碼經行重新編譯,在運作過程中不斷優化。此外你可以利用NGen.exe建立托管程式集的本機映像,運作該程式集時,就會自動使用該本機映像而不是JIT它們。這聽起來似乎很美妙,但是你必須做好以下準備:

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

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

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

  4.NGen.exe生成的代碼無法在運作時進行優化,無法直接通路靜态資源,也無法在應用程式域之間共享程式集。是以,除非你已十厘清楚程式性能是由于首次編譯造成的性能問題,否則盡量不要人工生成本地代碼。

  JIT很優秀,它不但有編譯的本事,還會根據記憶體資源情況換出使用率低的代碼,節省資源,這對于一些基于.Net平台的電子産品是很重要的。基于B/S模式運作的系統,如果使用率較高,可以基本忽略JIT帶來的性能損失,因為根據局部性原理與8020原則,常用的子產品都是編譯完畢的,隻有那些不常用的子產品,在第一次使用時會被編譯,并損失用一些時間。

繼續閱讀