經過了《泛型真的會降低性能嗎?》一文中的性能測試,已經從實際入手,從 測試數據上證明了泛型不會降低程序效率。只是還是有幾位朋友談到,“普遍認 為”泛型的代碼性能會略差一些,也有朋友正在進一步尋找泛型性能略差的證據 。老趙認為這種探究問題的方式非常值得提倡。不過,老趙忽然想到,如果從能 從匯編入手,證明非泛型和泛型的代碼之間沒有性能差距——好吧,或者說,存 在性能差距,那麼事情不就到此為止了嗎?任何理論說明,都抵不過觀察計算機 是如何處理這個問題來的“直接”。因此,老趙最終決定通過這種極端的方式來 一探究竟,把這個問題徹底解決。
需要一提的是,老趙並不希望這篇文章會引起一些不必要的爭論,因此一些話 就先說在前面。老趙並不喜歡用這種方式來解決問題。事實上,如果可以通過數 據比較,理論分析,或者高級代碼來說明問題,我連IL都不願意接觸,更別說深 入匯編。如果是平時的工作,就算使用WinDbg也最多是查看查看內存中有哪些數 據,系統到底出了哪些問題。如果您要老趙表態的話,我會說:我強烈反對接觸 匯編。我們有太多太多的東西需要學習,如果您並沒有明確您的目標,老趙建議 您就放過IL和匯編這種東西吧。我們知道這些是什麼就行了,不必對它們有什麼 “深入”的了解。
下面就要開始真正的探索之旅了。這不是一個順利的旅程,其中有些步驟是連 蒙帶猜,最後加以驗證才得到的結果。原本老趙打算按照自己的思路一步一步進 行下去,但是發現這樣太過冗余,反而會讓大家的思路難以集中。因此老趙最後 決定重新設計一個流程,和大家一起步步為營,朝著目標前進。此外,為了方便 某些朋友按照這文章親手進行操作,老趙也制作了一個dump文件,如果您是安裝 了.NET 3.5 SP1的32位x86系統,可以直接下載進行試驗。試驗過程中出現的地址 也會和文章中完全一致。
廢話就說到這裡,我們開始吧。
測試代碼
測試代碼便是我們的目標。和上一篇文章一樣,我們准備了一份最簡單的代碼 進行測試,這樣可以盡可能擺脫其他因素的影響,得到最正確的結果:
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<T> { 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<object> list = new MyList<object> (1); list[0] = list[0] ?? new object(); Console.WriteLine("Here comes the testing code."); var a = arrayList[0]; var b = list[0]; Console.ReadLine(); } } }
我們在這裡構建了兩個“容器”,一個是MyArrayList,另一個是 MyList<T>,前者直接使用Object類型,而後者則是一個泛型類。我們對兩 個類的索引屬性的get和set方法都加上了NoInlining標記,這樣便可以避免這種 簡單的方法被JIT內聯。而在Main方法中,前幾行代碼的作用都是構造兩個類的對 象,並確保索引的get和set方法都已經得到JIT。在打印出“Here comes the testing code.”之後,我們便對兩個類的實例進行“下標訪問”,並使控制台暫 停。
當Release編譯並運行之後,控制台會打印出“Here comes the testing code.”字樣並停止。這時候我們便可以使用WinDbg來Attach to Process進行調 試。老趙也是在這個時候制作了一個dump文件,您也可以Open Crash Dump命令打 開這個文件。更多操作您可以參考互聯網上的各篇文章,亦或是老趙之前寫過的 一篇《使用WinDbg獲得托管方法的匯編代碼》。
分析MyArrayList對象結構
假設您現在已經打開了WinDbg,並Attach to Process(或Open Crash Dump) ,而且加載了正確的sos.dll(可參考老趙之前給出的文章)。那麼第一件事情, 我們就要來分析一個MyArrayList對象的結構。
首先,我們還是在項目中查找MyArrayList類型的MT(Method Table,方法表 )地址:
0:000> !name2ee *!TestConsole.MyArrayList Module: 5bf71000 (mscorlib.dll) -------------------------------------- Module: 00362354 (sortkey.nlp) -------------------------------------- Module: 00362010 (sorttbls.nlp) -------------------------------------- Module: 00362698 (prcp.nlp) -------------------------------------- Module: 003629dc (mscorlib.resources.dll) -------------------------------------- Module: 00342ff8 (TestConsole.exe) Token: 0x02000002 MethodTable: 00343440 EEClass: 0034141c Name: TestConsole.MyArrayList
我們得到了MyArrayList類型的MT地址之後,便可以在系統中尋找MyArrayList 對象了:
0:000> !dumpheap -mt 00343440 Address MT Size 0205be3c 00343440 12 total 1 objects Statistics: MT Count TotalSize Class Name 00343440 1 12 TestConsole.MyArrayList Total 1 objects
不出所料,當前程序中只有一個MyArrayList對象。我們繼續追蹤它的地址:
0:000> !do 0205be3c Name: TestConsole.MyArrayList MethodTable: 00343440 EEClass: 0034141c Size: 12(0xc) bytes (E:\Users\Jeffrey Zhao\...\bin\Release\TestConsole.exe) Fields: MT Field Offset Type VT Attr Value Name 5c1b41d0 4000001 4 System.Object[] 0 instance 0205be48 m_items
OK,到這裡為止,我們得到一個結論。如果我們獲得了一個MyArrayList對象 的地址,那麼偏移4個字節,便可以得到m_items字段,也就是存放元素的Object 數組的地址。這點很關鍵,否則可能對於理解後面的匯編代碼形成障礙。
如果您使用同樣的方法來觀察MyList<object>類型的話,您會發現其結 果也完全相同:從對象地址開始偏移4個字節便是m_items字段,類型為Object數 組。
分析數組對象的結構
接著我們來觀察一下,一個數組對象在內存中的存放方式是什麼樣的。首先, 我們打印出托管堆上的各種類型:
0:000> !dumpheap -stat total 6922 objects Statistics: MT Count TotalSize Class Name 5c1e3ed4 1 12 System.Text.DecoderExceptionFallback 5c1e3e90 1 12 System.Text.EncoderExceptionFallback 5c1e1ea4 1 12 System.RuntimeTypeHandle 5c1dfb28 1 12 System.__Filters 5c1dfad8 1 12 System.Reflection.Missing 5c1df9e0 1 12 System.RuntimeType+TypeCacheQueue ... 5c1e3150 48 8640 System.Collections.Hashtable+bucket[] 5c1e2d28 347 9716 System.Collections.ArrayList+ArrayListEnumeratorSimple 5c1b5ca4 46 11024 System.Reflection.CustomAttributeNamedParameter[] 5c1cc590 404 11312 System.Security.SecurityElement 5c1e2a30 578 13872 System.Collections.ArrayList 5c1b50e4 335 14740 System.Int16[] 5c1b41d0 1735 87172 System.Object[] 5c1e0a00 718 167212 System.String 5c1e3470 70 174272 System.Byte[] Total 6922 objects
既然我們的代碼中使用了Object數組,那麼我們就把目標放在托管堆上的 Object數組中。從上面的信息中我們已經獲得了Object數組的MT地址,於是我們 繼續列舉出托管堆上的此類對象:
0:000> !dumpheap -mt 5c1b41d0 Address MT Size 01fd141c 5c1b41d0 80 01fd1c84 5c1b41d0 16 01fd1cc0 5c1b41d0 32 ... 0205baa4 5c1b41d0 20 0205bc4c 5c1b41d0 20 0205bc60 5c1b41d0 32 0205bdc4 5c1b41d0 16 0205be48 5c1b41d0 20 0205be74 5c1b41d0 20 0205c058 5c1b41d0 36 02fd1010 5c1b41d0 4096 02fd2020 5c1b41d0 528 02fd2240 5c1b41d0 4096 total 1735 objects Statistics: MT Count TotalSize Class Name 5c1b41d0 1735 87172 System.Object[] Total 1735 objects
我們隨意抽取一個Object數組對象,查看它的內容:
0:000> !do 02fd2020 Name: System.Object[] MethodTable: 5c1b41d0 EEClass: 5bf9da54 Size: 528(0x210) bytes Array: Rank 1, Number of elements 128, Type CLASS Element Type: System.Object Fields: None
WinDbg清楚明白地告訴我們,這個數組是1維的,共有128個元素。那麼這個數 組的長度信息是如何保存下來的呢(這個信息肯定是對象自帶的,這個很容易理 解吧)?我們直接查看這個數組對象地址上的數據吧:
0:000> dd 02fd2020 02fd2020 5c1b41d0 00000080 5c1e061c 01fd1198 02fd2030 0205bdf0 00000000 00000000 00000000 02fd2040 00000000 00000000 00000000 00000000 02fd2050 00000000 00000000 00000000 00000000 02fd2060 00000000 00000000 00000000 00000000 02fd2070 00000000 00000000 00000000 00000000 02fd2080 00000000 00000000 00000000 00000000 02fd2090 00000000 00000000 00000000 00000000
十六進制數00000080不就是十進制的128嗎?沒錯,老趙對多個數組對象進行 分析之後,發現數組對象存放的結構是從對象的地址開始:
偏移0字節:存放了這個數組對象的MT地址,例如上面的5c1b41d0便是Object []類型的MT地址。
偏移4字節:存放了數組長度。
偏移8字節:存放了數組元素類型的MT地址,例如上面的5c1e061c便是Object 類型的MT地址,您可以使用!dumpmt -md 5c1e061c指令進行觀察。
偏移12字節:從這裡開始,便存放了數組的每個元素了。也就是說,如果這是 一個引用類型的數組,那麼偏移12字節則存放了第1個(下標為0)元素的地址, 偏移16字節則存放第2個元素的地址,以此類推。
實際上,這些是老趙在自己的試驗過程中,從接下去會講解的匯編代碼出發猜 測出來的結果,經過驗證發現恰好符合。為了避免您走這些彎路,老趙就先將這 一結果告訴大家了。
分析Main函數的匯編代碼
接下去便要觀察Main函數的匯編代碼了。獲取匯編代碼的方法很簡單,如果您 對此還不太了解,老趙的文章《使用WinDbg獲得托管方法的匯編代碼》會給您一 定幫助。Main函數的匯編代碼如下:
0:000> !u 01d40070 Normal JIT generated code TestConsole.Program.Main(System.String[]) Begin 01d40070, size e2 >>> 01d40070 push ebp 01d40071 mov ebp,esp 01d40073 push edi 01d40074 push esi 01d40075 push ebx ... 01d4011d mov ecx,eax // 打印字樣“Here comes the testing code.” 01d4011f mov edx,dword ptr ds:[2FD2030h] ("Here comes the testing code.") 01d40125 mov eax,dword ptr [ecx] 01d40127 call dword ptr [eax+0D8h] // 將MyArrayList對象的地址保存在ecx寄存器中 01d4012d mov ecx,esi // 將edx寄存器清零,作為訪問下面get_Item方法的參數 01d4012f xor edx,edx // 獲取地址0x343424中的數據(它是get_Item方法的訪問入口),並調用 01d40131 call dword ptr ds:[343424h] (...MyArrayList.get_Item (Int32), ...) // 將MyList<object>對象的地址保存在ecx寄存器中 01d40137 mov ecx,edi // 將edx寄存器清零,作為訪問下面get_Item方法的參數 01d40139 xor edx,edx // 獲取地址0x343594中的數據(它是get_Item方法的訪問入口),並調用 01d4013b call dword ptr ds:[343594h] (...MyList`1[...].get_Item (Int32), ...) // 調用Console.ReadLine方法,請注意靜態方法不需要把對象地址放到ecx寄存 器中 01d40141 call mscorlib_ni+0x6d1af4 (5c641af4) (System.Console.get_In(), ...) 01d40146 mov ecx,eax 01d40148 mov eax,dword ptr [ecx] 01d4014a call dword ptr [eax+64h] 01d4014d pop ebx 01d4014e pop esi 01d4014f pop edi 01d40150 pop ebp 01d40151 ret
總結
還需要多說什麼嗎?我們通過比較匯編代碼,已經證明了MyArrayList和 MyList<Object>在執行時所經過的指令幾乎完全相同。到了這個地步,您 是否還認為泛型會影響程序性能?
最後繼續強調一句:老趙並不喜歡IL,更不喜歡匯編。除非萬不得已,老趙是 不會往這方面去思考問題的。我們有太多東西可學,如果不是目標明確,老趙建 議您還是不要投身於IL或匯編這類東西為好。