上一篇文章中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還沒有進行編譯,只是建立了方法表!
圖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命令遍曆所有已載入模組,如:
圖3 查看類型資訊
斷行符號後注意高亮地區的資訊:
圖4 JIT前A類型的資訊
高亮地區顯示的是“”,這說明雖然運行和程式,但未點擊按鈕時,A類型未被JIT,因為它還沒有入口地址。這一點體現了即時、按需編譯的思想。
同樣,!name2ee *!JITTester.B和!name2ee *!JITTester.C命令會得到同樣的結果。
好,現在繼續,Detach Debuggee進程,並回到程式中點擊“GO”按鈕
圖5 點擊按鈕
然後重新附加進程,這時程式已經調用了new A().a1()方法,並重新執行令!name2ee *!JITTester.A ,注意高亮部分
圖6 JIT後A類型的資訊>
和圖4中的資訊比較,圖6中的方法表地址已經變為JIT後的記憶體位址,這時圖2中的Stub槽將被一條強制跳躍陳述式替換,跳轉目標與該地址有關。這一點說明JIT在大多情況下,只編譯一次代碼。
同樣命令查看B類型:
圖7 JIT後B類型的資訊
該類型未被調用,所以還未被JIT。
C類型:
圖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原則,常用的模組都是編譯完畢的,只有那些不常用的模組,在第一次使用時會被編譯,並損失用一些時間。
未完
下一篇中,Aicken將會為大家介紹.Net Exception機制、字串駐留機制以及其帶來的效能問題,敬請期待。
我是Aicken(李鳴) 請您繼續關注我的下一篇文章。
“.Net Discovery 系列”是講解.Net平台本質的文章,現在已經有:
.Net Discovery 系列之七--深入理解.Net垃圾收集機制(拾貝篇) 發布在新年第一秒
.Net Discovery 系列之五--Me JIT(深入淺出.Net JIT 上)
.Net Discovery 系列之六--Me JIT(深入淺出.Net JIT 下)
.Net Discovery 系列之三--深入理解.Net垃圾收集機制(上)
.Net Discovery 系列之四--深入理解.Net垃圾收集機制(下)
.Net Discovery 系列之一--string從入門到精通(上)
.Net Discovery 系列之二--string從入門到精通(下)