结构体,三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读

上次我分别测试了类与结构体(http://www.cnblogs.com/zyl910/archive/2011/09/19/2186623.html)、密封类(http://www.cnblogs.com/zyl910/archive/2011/09/20/2186622.html)的函数调用速度评测。现在进行进一步分析,解读编译器生成的MSIL(微软中间语言)代码。
一、前期准备
先找到“IL 反汇编程序”(开始\程序\Microsoft Visual Studio 2010\Microsoft Windows SDK Tools\)——
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读结构体
运行“IL 反汇编程序”,打开编译后的exe。展开节点,双击叶子节点查看MSIL代码——
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读结构体
二、结果分析
然后我们将测试函数调用的那行代码复制提取出来。如上图的“IL_004c”行。 在复制提取过程中,发现VS2005与VS2010生成的函数调用代码是完全一样的。删除啰嗦的名称空间,将结果整理为表格——
模式 MSIL 亮点
静态调用 call uint8* TryIt_Static_Ptr(uint8*) 静态函数
调用派生类 callvirt instance uint8* PointerCall::Ptr(uint8*) 虚方法
调用密封类 callvirt instance uint8* SldPointerCallAdd::Ptr(uint8*) 虚方法
调用结构体 call instance uint8* SPointerCallAdd::Ptr(uint8*) 方法(非虚)
调用基类 callvirt instance uint8* PointerCall::Ptr(uint8*) 虚方法
调用派生类的接口 callvirt instance uint8* IPointerCall::Ptr(uint8*) 虚方法
调用密封类的接口 callvirt instance uint8* IPointerCall::Ptr(uint8*) 虚方法
调用结构体的接口 callvirt instance uint8* IPointerCall::Ptr(uint8*) 虚方法
基类泛型调用派生类 call uint8* CallClassPtr(!!0, uint8*) class
基类泛型调用基类 call uint8* CallClassPtr(!!0, uint8*) class
接口泛型调用派生类 call uint8* CallPtr(!!0, uint8*) class
接口泛型调用密封类 call uint8* CallPtr(!!0, uint8*) class
接口泛型调用结构体 call uint8* CallPtr(!!0, uint8*) valuetype
接口泛型调用结构体引用 call uint8* CallRefPtr(!!0&, uint8*) valuetype
接口泛型调用基类 call uint8* CallPtr(!!0, uint8*) class
接口泛型调用派生类的接口 call uint8* CallPtr(!!0, uint8*) class
接口泛型调用密封类的接口 call uint8* CallPtr(!!0, uint8*) class
接口泛型调用结构体的接口 call uint8* CallPtr(!!0, uint8*) class
观察上面的表格,我们发现—— 1.编译的IL代码时,并没有做内联(inline。将子函数展开)优化,而根据语义统统编译为不同的调用(call)。看来优化工作是JIT(即时编译器)负责的。 2.调用结构体是 方法调用(call instance)。JIT可根据此信息安排内联优化。 3.调用派生类是 虚方法调用(callvirt instance)。因为被编译为 调用基类的虚方法(PointerCall::Ptr),所以JIT认为其是正常的虚方法调用,不优化。 4.调用密封类是 虚方法调用(callvirt instance),与派生类调用一致。但由于其留下了类型信息(SldPointerCallAdd::Ptr),JIT发现它是一个密封类,于是安排内联优化。 5.泛型方法虽然也是用call指令,但它带有泛型参数,所以其行为与普通call调用不同。 6.结构体调用泛型方法时,会使用valuetype关键字。JIT可根据此信息安排优化(VS005的JIT有所优化;而VS2010的JIT将其进行彻底的内联优化)。
附录A、转为接口时的IL代码
派生类转为接口—— IL_001d: ldloc.0 IL_001e: stloc.s V_4
密封类转为接口—— IL_0020: ldloc.1 IL_0021: stloc.s V_5
结构体转为接口—— IL_0023: ldloc.2 IL_0024: box TryPointerCall.SPointerCallAdd IL_0029: stloc.s V_6
可见结构体转为接口时多了装箱操作,影响了性能。
附录B、结构体泛型调用的IL代码
接口泛型调用结构体—— IL_0391: ldloc.2 IL_0392: ldloc.s V_7 IL_0394: call uint8* TryPointerCall.PointerCallTool::CallPtr(!!0,uint8*)
接口泛型调用结构体引用—— IL_03dd: ldloca.s V_2 IL_03df: ldloc.s V_7 IL_03e1: call uint8* TryPointerCall.PointerCallTool::CallRefPtr(!!0&,uint8*)
可见泛型调用的IL代码并不复杂,与普通调用基本一样,也是先将参数放入堆栈再call。对于引用参数,将“ldloc.*”指令换成“ldloca.s”指令就行了。
(完)
Tags: 

延伸阅读

最新评论

发表评论