找回密码
 注册帐号
查看: 1562|回复: 0

从汇编入手,探究泛型的性能疑问

[复制链接]
发表于 2017-1-1 18:51:16 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册帐号

×
经过了《泛型真的会降低性能吗?》一文中的性能测试,已经从实际入手,从测试数据上证明了泛型不会降低程序效率。只是还是有几位朋友谈到,“普遍认为”泛型的代码性能会略差一些,也有朋友正在进一步寻找泛型性能略差的证据。老赵认为这种探究疑问的方式非常值得提倡。不过,老赵忽然想到,如果从能从汇编入手,证明非泛型和泛型的代码之间没有性能差距——好吧,或者说,存在性能差距,那么事情不就到此为止了吗?任何理论说明,都抵不过观察计算机是如何 处理这个疑问来的“直接”。因此,老赵最终决定通过这种极端的方式来一探究竟,把这个疑问彻底处理。
  须要一提的是,老赵并不希望这篇文章会引起一些不必要的争论,因此一些话就先说在前面。老赵并不喜欢用这种方式来处理疑问。事实上,如果可以通过数据比较,理论分析,或者高级代码来说明疑问,我连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:UsersJeffrey Zhao...binReleaseTestConsole.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
老赵为上面这段汇编代码添加了注释,我们主要从打印出“Here comes the testing code.”字样的代码开始执行 分析。值得留心的是,在调用MyArrayList或MyList<object>的get_Item要领之前,都会把这个对象的地址放置到ecx寄存器中,然后把edx寄存器清零作为get_Item要领的参数。这样做的优点是加快访问对象及参数的速度,如果每次都须要从线程栈上读取这些(就像我们学习汇编时的那些经典案例),其性能肯定比不上读取寄存器。显然,调用Console.ReadLine静态要领是不须要对象地址的,因此无须对ecx寄存器有所操作。
  分析get_Item要领的汇编代码
  从Main函数的汇编代码中我们可以获得get_Item要领的入口。那么我们现在就来分析MyArrayList类型的get_Item要领,请留心,此时ecx寄存器保存的是MyArrayList对象的地址,edx保存了get_Item要领的参数:
0:000> dd 343424h
00343424 01d40168 71060003 20000006 01d40190
00343434 fffffff8 00000004 00000001 00080000
00343444 0000000c 00040011 00000004 5c1e061c
00343454 00342ff8 00343478 0034141c 00000000
00343464 00000000 5c136aa0 5c136ac0 5c136b30
00343474 5c1a7410 00000080 00000000 003434c0
00343484 10000002 90000000 003434c0 00000000
00343494 0034c05c 00020520 00000004 00000004
0:000> !u 01d40168
Normal JIT generated code
TestConsole.MyArrayList.get_Item(Int32)
Begin 01d40168, size 17
>>> 01d40168 55       push  ebp
01d40169 8bec      mov   ebp,esp
// 把MyArrayList对象的m_items字段地址(对象地址偏移4字节)保存至eax寄存器中
01d4016b 8b4104     mov   eax,dword ptr [ecx+4]
// 比较传入的参数(edx寄存器)与数组长度(eax寄存器为数组地址,再偏移4字节)的大小
01d4016e 3b5004     cmp   edx,dword ptr [eax+4]
// 如果参数超过数组长度,则跳转至不正确处理代码
01d40171 7306      jae   01d40179
// 把须要的元素地址放置到eax寄存器中
// 从数组地址开始偏移12字节为第一个元素的地址,再偏移“下标 * 4”自然就是我们所须要的元素
01d40173 8b44900c    mov   eax,dword ptr [eax+edx*4+0Ch]
01d40177 5d       pop   ebp
// 返回
01d40178 c3       ret
// 如果参数大于数组长度,就会跳转到此
01d40179 e806c2a15c   call  mscorwks!JIT_RngChkFail (5e75c384)
01d4017e cc       int   3
如果要理解上面的代码,可能须要您再去回味文章上半段的分析。尤其是多个偏移量:
  MyArrayList对象偏移4字节则为m_items字段地址
  数组地址偏移4字节则为其长度
  数组地址偏移12字节为其第一个元素的地址
  然后,再结合ecx(MyArrayList对象地址),edx(参数)以及eax(保存了要领返回值)多个寄存器的作用,相信理解上面这段代码也并非难事。
  MyArrayList的代码分析完了,那么MyList<object>的汇编代码又是如何 ?
0:000> dd 343594h
00343594 01d401b8 01d401e0 00010001 003435a4
003435a4 5c1e0670 00000000 00000000 00000080
003435b4 00000000 fffffff8 00000004 00000001
003435c4 00080010 0000000c 00040011 00000004
003435d4 5c1e061c 00342ff8 00343610 0034355a
003435e4 00343600 00000000 5c136aa0 5c136ac0
003435f4 5c136b30 5c1a7410 00010001 00343604
00343604 5c1e061c 00000000 00000000 00000080
0:000> !u 01d401b8
Normal JIT generated code
TestConsole.MyList`1[[System.__Canon, mscorlib]].get_Item(Int32)
Begin 01d401b8, size 17
>>> 01d401b8 55       push  ebp
01d401b9 8bec      mov   ebp,esp
01d401bb 8b4104     mov   eax,dword ptr [ecx+4]
01d401be 3b5004     cmp   edx,dword ptr [eax+4]
01d401c1 7306      jae   01d401c9
01d401c3 8b44900c    mov   eax,dword ptr [eax+edx*4+0Ch]
01d401c7 5d       pop   ebp
01d401c8 c3       ret
01d401c9 e8b6c1a15c   call  mscorwks!JIT_RngChkFail (5e75c384)
01d401ce cc       int   3
能不能发觉,两者的代码除了多个地址之外可以说完全一样?
  总结
  还须要多说什么吗?我们通过比较汇编代码,已经证明了MyArrayList和MyList<Object>在执行时所经过的指令几乎完全相同。到了这个地步,您能不能还认为泛型会影响程序性能?
  最后继续强调一句:老赵并不喜欢IL,更不喜欢汇编。除非万不得已,老赵是不会往这方面去思考疑问的。我们有太多东西可学,如果不是目标明确,老赵建议您还是不要投身于IL或汇编这类东西为好。
欢迎来到安全之家
悄悄告诉你善用本站的【 搜索 】功能,那里可能会有你要找的答案哦
您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

Archiver|sitemap|小黑屋|手机版原版|安全之家

GMT+8, 2025-4-11 06:31 , Processed in 0.044999 second(s), 5 queries , Gzip On, Redis On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表