.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尚未進行編譯,只是創建了方法表!html

 

圖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原則,經常使用的模塊都是編譯完畢的,只有那些不經常使用的模塊,在第一次使用時會被編譯,並損失用一些時間。

未完spa

 

轉自:http://www.cnblogs.com/isline/archive/2010/04/07/1705966.html操作系統

相關文章
相關標籤/搜索