上次我分別測試了類與結構體(http://www.cnblogs.com/zyl910/archive/2011/09/19/2186623.html)、密封類(http://www.cnblogs.com/zyl910/archive/2011/09/20/2186622.html)的函數調用速度評測。現在進行進一步分析,解讀編譯器產生的MSIL(微軟中繼語言)代碼。
一、前期準備
先找到“IL 反組譯工具”(開始\程式\Microsoft Visual Studio 2010\Microsoft Windows SDK Tools\)——
運行“IL 反組譯工具”,開啟編譯後的exe。展開節點,雙擊葉子節點查看MSIL代碼——
二、結果分析
然後我們將測試函數調用的那行代碼複製提取出來。如的“IL_004c”行。
在複製提取過程中,發現VS2005與VS2010產生的函數調用代碼是完全一樣的。刪除囉嗦的名稱空間,將結果整理為表格——
模式 |
MSIL |
亮點 |
靜態調用 |
call uint8* TryIt_Static_Ptr(uint8*) |
靜態函數 |
調用衍生類別 |
callvirt instance uint8* PointerCall::Ptr(uint8*) |
虛方法 |
調用密封類 |
callvirt instance uint8* SldPointerCallAdd::Ptr(uint8*) |
虛方法 |
調用結構體 |
call instance uint8* SPointerCallAdd::Ptr(uint8*) |
方法(非虛) |
調用基類 |
callvirt instance uint8* PointerCall::Ptr(uint8*) |
虛方法 |
調用衍生類別的介面 |
callvirt instance uint8* IPointerCall::Ptr(uint8*) |
虛方法 |
調用密封類的介面 |
callvirt instance uint8* IPointerCall::Ptr(uint8*) |
虛方法 |
調用結構體的介面 |
callvirt instance uint8* IPointerCall::Ptr(uint8*) |
虛方法 |
基類泛型調用衍生類別 |
call uint8* CallClassPtr<class PointerCallAdd>(!!0, uint8*) |
class |
基類泛型調用基類 |
call uint8* CallClassPtr<class PointerCall>(!!0, uint8*) |
class |
介面泛型調用衍生類別 |
call uint8* CallPtr<class PointerCallAdd>(!!0, uint8*) |
class |
介面泛型調用密封類 |
call uint8* CallPtr<class SldPointerCallAdd>(!!0, uint8*) |
class |
介面泛型調用結構體 |
call uint8* CallPtr<valuetype SPointerCallAdd>(!!0, uint8*) |
valuetype |
介面泛型調用結構體引用 |
call uint8* CallRefPtr<valuetype SPointerCallAdd>(!!0&, uint8*) |
valuetype |
介面泛型調用基類 |
call uint8* CallPtr<class PointerCall>(!!0, uint8*) |
class |
介面泛型調用衍生類別的介面 |
call uint8* CallPtr<class IPointerCall>(!!0, uint8*) |
class |
介面泛型調用密封類的介面 |
call uint8* CallPtr<class IPointerCall>(!!0, uint8*) |
class |
介面泛型調用結構體的介面 |
call uint8* CallPtr<class IPointerCall>(!!0, uint8*) |
class |
觀察上面的表格,我們發現——
1.編譯的IL代碼時,並沒有做內聯(inline。將子函數展開)最佳化,而根據語義統統編譯為不同的調用(call)。看來最佳化工作是JIT(即時編譯器)負責的。
2.調用結構體是 方法調用(call instance)。JIT可根據此資訊安排內聯最佳化。
3.調用衍生類別是 虛方法調用(callvirt instance)。因為被編譯為 調用基類的虛方法(PointerCall::Ptr),所以JIT認為其是正常的虛方法調用,不最佳化。
4.調用密封類是 虛方法調用(callvirt instance),與衍生類別調用一致。但由於其留下了類型資訊(SldPointerCallAdd::Ptr),JIT發現它是一個密封類,於是安排內聯最佳化。
5.泛型方法雖然也是用call指令,但它帶有泛型參數,所以其行為與普通call調用不同。
6.結構體調用泛型方法時,會使用valuetype關鍵字。JIT可根據此資訊安排最佳化(VS005的JIT有所最佳化;而VS2010的JIT將其進行徹底的內聯最佳化)。
附錄A、轉為介面時的IL代碼
衍生類別轉為介面——
IL_001d: ldloc.0
IL_001e: stloc.s V_4
密封類轉為介面——
IL_0020: ldloc.1
IL_0021: stloc.s V_5
結構體轉為介面——
IL_0023: ldloc.2
IL_0024: box TryPointerCall.SPointerCallAdd
IL_0029: stloc.s V_6
可見結構體轉為介面時多了裝箱操作,影響了效能。
附錄B、結構體泛型調用的IL代碼
介面泛型調用結構體——
IL_0391: ldloc.2
IL_0392: ldloc.s V_7
IL_0394: call uint8* TryPointerCall.PointerCallTool::CallPtr<valuetype TryPointerCall.SPointerCallAdd>(!!0,uint8*)
介面泛型調用結構體引用——
IL_03dd: ldloca.s V_2
IL_03df: ldloc.s V_7
IL_03e1: call uint8* TryPointerCall.PointerCallTool::CallRefPtr<valuetype TryPointerCall.SPointerCallAdd>(!!0&,uint8*)
可見泛型調用的IL代碼並不複雜,與普通調用基本一樣,也是先將參數放入堆棧再call。對於引用參數,將“ldloc.*”指令換成“ldloca.s”指令就行了。
(完)
目錄——
C#類與結構體究竟誰快——各種函數調用模式速度評測:http://www.cnblogs.com/zyl910/archive/2011/09/19/2186623.html
再探C#類與結構體究竟誰快——考慮棧變數、棧分配、64位整數、密封類:http://www.cnblogs.com/zyl910/archive/2011/09/20/2186622.html
三探C#類與結構體究竟誰快——MSIL(微軟中繼語言)解讀:http://www.cnblogs.com/zyl910/archive/2011/09/24/2189403.html
四探C#類與結構體究竟誰快——跨程式集(assembly)調用:http://www.cnblogs.com/zyl910/archive/2011/10/01/2197844.html