经过了《泛型真的会降低性能吗?》一文中的性能测试,已经从实际入手,从测试数据上证明了泛型不会降低程序效率。只是还是有几位朋友谈到,“普遍认为”泛型的代码性能会略差一些,也有朋友正在进一步寻找泛型性能略差的证据。老赵认为这种探究问题的方式非常值得提倡。不过,老赵忽然想到,如果从能从汇编入手,证明非泛型和泛型的代码之间没有性能差距——好吧,或者说,存在性能差距,那么事情不就到此为止了吗?任何理论说明,都抵不过观察计算机是如何处理这个问题来的“直接”。因此,老赵最终决定通过这种极端的方式来一探究竟,把这个问题彻底解决。
需要一提的是,老赵并不希望这篇文章会引起一些不必要的争论,因此一些话就先说在前面。老赵并不喜欢用这种方式来解决问题。事实上,如果可以通过数据比较,理论分析,或者高级代码来说明问题,我连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,前者直接使用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> <strong>!name2ee *!TestConsole.MyArrayList</strong>
- 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
- <font color="#ff0000">MethodTable: 00343440</font>
- EEClass: 0034141c
- Name: TestConsole.MyArrayList
复制代码 我们得到了MyArrayList类型的MT地址之后,便可以在系统中寻找MyArrayList对象了:- 0:000> <strong>!dumpheap -mt 00343440</strong>
- <font color="#ff0000">Address</font> MT Size
- <font color="#ff0000">0205be3c</font> 00343440 12
- total 1 objects
- Statistics:
- MT Count TotalSize Class Name
- 00343440 1 12 TestConsole.MyArrayList
- Total 1 objects
复制代码 不出所料,当前程序中只有一个MyArrayList对象。我们继续追踪它的地址:- 0:000> <strong>!do 0205be3c</strong>
- Name: TestConsole.MyArrayList
- MethodTable: 00343440
- EEClass: 0034141c
- Size: 12(0xc) bytes
- (E:\Users\Jeffrey Zhao\...\bin\Release\TestConsole.exe)
- Fields:
- MT Field <font color="#ff0000">Offset</font> Type VT Attr Value Name
- 5c1b41d0 4000001 <font color="#ff0000">4</font> System.Object[] 0 instance 0205be48 m_items
复制代码 OK,到这里为止,我们得到一个结论。如果我们获得了一个MyArrayList对象的地址,那么偏移4个字节,便可以得到m_items字段,也就是存放元素的Object数组的地址。这点很关键,否则可能对于理解后面的汇编代码形成障碍。
如果您使用同样的方法来观察MyList类型的话,您会发现其结果也完全相同:从对象地址开始偏移4个字节便是m_items字段,类型为Object数组。
分析数组对象的结构
接着我们来观察一下,一个数组对象在内存中的存放方式是什么样的。首先,我们打印出托管堆上的各种类型:- 0:000> <strong>!dumpheap -stat</strong>
- 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[]
- <font color="#ff0000">5c1b41d0 1735 87172 System.Object[]</font>
- 5c1e0a00 718 167212 System.String
- 5c1e3470 70 174272 System.Byte[]
- Total 6922 objects
复制代码 既然我们的代码中使用了Object数组,那么我们就把目标放在托管堆上的Object数组中。从上面的信息中我们已经获得了Object数组的MT地址,于是我们继续列举出托管堆上的此类对象:- 0:000> <strong>!dumpheap -mt 5c1b41d0</strong>
- 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
- <font color="#ff0000">02fd2020</font> 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> <strong>!do 02fd2020</strong>
- Name: System.Object[]
- MethodTable: 5c1b41d0
- EEClass: 5bf9da54
- Size: 528(0x210) bytes
- <font color="#ff0000">Array: Rank 1, Number of elements 128, Type CLASS</font>
- Element Type: System.Object
- Fields:
- None
复制代码 WinDbg清楚明白地告诉我们,这个数组是1维的,共有128个元素。那么这个数组的长度信息是如何保存下来的呢(这个信息肯定是对象自带的,这个很容易理解吧)?我们直接查看这个数组对象地址上的数据吧:- 0:000> <strong>dd 02fd2020</strong>
- 02fd2020 5c1b41d0 <font color="#ff0000">00000080</font> 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> <strong>!u 01d40070</strong>
- 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的get_Item方法之前,都会把这个对象的地址放置到ecx寄存器中,然后把edx寄存器清零作为get_Item方法的参数。这样做的好处是加快访问对象及参数的速度,如果每次都需要从线程栈上读取这些(就像我们学习汇编时的那些经典案例),其性能肯定比不上读取寄存器。显然,调用Console.ReadLine静态方法是不需要对象地址的,因此无须对ecx寄存器有所操作。
分析get_Item方法的汇编代码
从Main函数的汇编代码中我们可以获得get_Item方法的入口。那么我们现在就来分析MyArrayList类型的get_Item方法,请注意,此时ecx寄存器保存的是MyArrayList对象的地址,edx保存了get_Item方法的参数:- 0:000> <strong>dd 343424h</strong>
- 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> <strong>!u 01d40168</strong>
- 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的汇编代码又是如何?- 0:000> <strong>dd 343594h</strong>
- 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> <strong>!u 01d401b8</strong>
- 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在执行时所经过的指令几乎完全相同。到了这个地步,您是否还认为泛型会影响程序性能?
最后继续强调一句:老赵并不喜欢IL,更不喜欢汇编。除非万不得已,老赵是不会往这方面去思考问题的。我们有太多东西可学,如果不是目标明确,老赵建议您还是不要投身于IL或汇编这类东西为好。
最后附上dump文件。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |