程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> 從匯編入手,探究泛型的性能問題

從匯編入手,探究泛型的性能問題

編輯:匯編語言

經過了《泛型真的會降低性能嗎?》一文中的性能測試,已經從實際入手,從 測試數據上證明了泛型不會降低程序效率。只是還是有幾位朋友談到,“普遍認 為”泛型的代碼性能會略差一些,也有朋友正在進一步尋找泛型性能略差的證據 。老趙認為這種探究問題的方式非常值得提倡。不過,老趙忽然想到,如果從能 從匯編入手,證明非泛型和泛型的代碼之間沒有性能差距——好吧,或者說,存 在性能差距,那麼事情不就到此為止了嗎?任何理論說明,都抵不過觀察計算機 是如何處理這個問題來的“直接”。因此,老趙最終決定通過這種極端的方式來 一探究竟,把這個問題徹底解決。

需要一提的是,老趙並不希望這篇文章會引起一些不必要的爭論,因此一些話 就先說在前面。老趙並不喜歡用這種方式來解決問題。事實上,如果可以通過數 據比較,理論分析,或者高級代碼來說明問題,我連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或匯編這類東西為好。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved