一、IL與匯編語言
IL是微軟.NET平臺上衍生出的一門中間語言,.NET平臺上的各種高級語言(如C#,VB,F#)的編譯器會將各自的代碼轉化為IL。,其中包含了.NET平臺上的各種元素,如“范型”,“類”、、“接口”、“模塊”、“屬性”等等。值得注意的是,各種高級語言本身可能根本沒有這些“概念”在里頭,如IronScheme是一個在.NET平臺上的Scheme語言實現,其中根本沒有前面提到的這些IL——亦或說是.NET平臺上的名詞。IL本身并不知道自己是由哪種高級語言轉化而來的,哪種語言中有哪些特性,IL也根本不會關心。
各種語言的編譯器將:高級語言=> IL。
匯編是讓CPU直接使用的“語言”,請注意“直接”二字:一條匯編指令便是讓CPU作一件事情(如寄存器的復制,從內存中讀取數據等等),毫無二義。不同族CPU擁有不同的指令集,但是它們都有一樣的特征:指令的數量相對較少,每個指令功能都簡單之至。
由于CPU只認識匯編代碼(機器碼和匯編其實也是一一對應的,您可以這樣理解:匯編是機器碼的文字表現形式,提供了一些方便人們記憶的“助記符”),因此就算是IL也需要再次進行轉化,才能被CPU執行。這次轉化便由“JIT Compiler”(即時編譯器)完成。CLR加載了IL之后,當每個方法——請注意這是IL中的概念——第一次被執行時,就會使用JIT將IL代碼進行編譯為機器碼。與IL不同的是,CLR,JIT都是真正了解CPU的,對于同樣的IL,JIT會把它為不同的CPU架構(如x86/IA64等等)生成不同的機器碼。這也是Java/.NET中“Compile Once,Run Everywhere”這一口號的技術基礎:它們為不同的CPU架構提供了不同的“IL轉化器”,僅此而已。與高級語言到IL的轉化類似,CPU也完全不知道自己在執行的指令是從哪里來的,可能是JIT從IL轉化而來,可能是JVM從Java Bytecode轉化而來,也有可能是C語言編譯得來,也有可能是由MIT/GNU Scheme解釋而來。
這就是.NET平臺上的高級語言在機器上運行的第二次轉化:IL =>匯編(機器碼)。
因此,IL和匯編的區別是顯著的。IL擁有各種高級特性,它知道什么是范型,什么是類和方法(以及它們的“名稱”),什么是繼承,什么是字符串,布爾值,什么是User對象。而CPU只知道寄存器,地址,內存,01010101。與匯編相比,IL簡直太高級了,幾乎完全是一個高級語言,比C語言還要高級。因此,您會看到.NET Reflector幾乎可以把IL代碼“一五一十”地反編譯為可讀性良好的C#代碼,包括類,屬性,方法等等;而從匯編只能勉勉強強地反編譯為C語言——而且其中的“方法名”等信息已經完全不可恢復了,更別說“模塊”等高級抽象的內容。您想要把匯編反編譯成C#代碼?相信在將來這是可行的,不過現在這還是天方夜譚。
二、IL并不是萬能的,CLR還有很多內容IL都無法看到
示例一:探究泛型在某些情況下的性能問題
namespace TestConsole
{
public class MyArrayList
{
public MyArrayList(int length)
{
this.m_items = new object[length];
}
private object[] m_items;
public object this[int index]
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
return this.m_items[index];
}
[MethodImpl(MethodImplOptions.NoInlining)]
set
{
this.m_items[index] = value;
}
}
}
public class MyList
{
public MyList(int length)
{
this.m_items = new T[length];
}
private T[] m_items;
public T this[int index]
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
return this.m_items[index];
}
[MethodImpl(MethodImplOptions.NoInlining)]
set
{
this.m_items[index] = value;
}
}
}
class Program
{
static void Main(string[] args)
{
MyArrayList arrayList = new MyArrayList(1);
arrayList[0] = arrayList[0] ?? new object();
MyList list = new MyList(1);
list[0] = list[0] ?? new object();
Console.WriteLine("Here comes the testing code.");
var a = arrayList[0];
var b = list[0];
Console.ReadLine();
}
}
}
示例目的是證明“.NET中,就算在使用Object作為泛型類型的時候,也不會比直接使用Object類型性能差”。類MyList泛型容器,類MyArrayList直接使用Object類型的容器。在Main方法中將對MyList和MyArrayList的下標索引進行訪問。至此,便出現了一些疑問,為泛型容器使用Object類型,是否比直接使用Object類型性能要差?看MyArrayList.get_Item和MyList.get_Item兩個方法的IL代碼get操作:
// MyArrayList的get_Item方法
.method public hidebysig specialname instance object get_Item(int32 index) cil managed noinlining
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldfld object[] TestConsole.MyArrayList::m_items
L_0006: ldarg.1
L_0007: ldelem.ref
L_0008: ret
}
// MyList的get_Item方法
.method public hidebysig specialname instance !T get_Item(int32 index) cil managed noinlining
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldfld !0[] TestConsole.MyList`1::m_items
L_0006: ldarg.1
L_0007: ldelem.any !T
L_000c: ret
}
這兩個方法的區別只在于紅色的兩句。我們“默認”ldfld指令的功能在兩段代碼中產生的效果完全相同(畢竟是相同的指令嘛),但是您覺得ldelem.ref指令和ldelem.any兩條指令的效果如何,它們是一樣的嗎?我們通過查閱一些資料可以了解到說,ldelem.any的作用是加載一個泛型向量或數組中的元素。不過它的性能如何?您能得出結果說,它就和ldelem.ref指令一樣嗎?
除非您了解到JIT對待這兩個指令的具體方式,否則您是無法得出其中性能高低的。因為IL還是過于高級,您看到了一條IL指令,您可以知道它的作用,但是您還是不知道它最終造成了何種結果。您還是無法證明“Object泛型集合的性能不會低于直接存放Object的非泛型集合”。因此,比較MyArrayList.get_Item方法和MyList.get_Item方法的匯編代碼,最后得出結果是“毫無二致”。由于匯編代碼和機器代碼一一對應,因此觀察匯編代碼就可以完全了解CPU是如何執行這兩個方法的。匯編代碼一模一樣,就意味著CPU對待這兩個方法的方式一模一樣,它們的性能怎么會有不同呢?
結論:.NET的Object泛型容器的性能不會低于直接使用Object的容器,因為CLR在處理Object泛型的時候,會生成與直接使用Object類型時一模一樣的類型,因此性能是不會降低的。但是您是通過學習IL可以了解這些嗎?顯然不是,如果您只是學習了IL,最終還是要“聽別人說”才能知道這些,而即使您不學IL,在“聽別人說”了之后您也了解了這些——同時也不會因為不了解IL而變得“易忘”等等。
同樣道理,IL的call指令和callvirt指令的區別是什么呢?“別人會告訴你”call指令直接就去調用了那個方法,而callvirt還需要去虛方法表里去“尋找”那個真正的方法;“別人可能還會告訴你”,查找虛方法是靠方法表地址加偏移量;《Essential .NET》還會將方法表的實現結構告訴給你,而這些都是IL不會告訴您的。您就算了解再多IL,也不如“別人告訴你”的這些來得重要。您要了解“別人告訴你”的東西,也不需要了解多少IL。
示例二:只有經過調用的方法才能獲得其匯編代碼嗎?
許多資料都告訴我們,在一個方法被第一次調用之前,它是不會被JIT的。也就是說,直到第一次調用時它才會被轉化為機器碼。不過,這個真是這樣嗎?我們還是準備一段簡單的C#代碼: