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

上次我分别测试了类与结构体(http://blog.csdn.net/zyl910/article/details/6788417)、密封类(http://blog.csdn.net/zyl910/article/details/6793908)的函数调用速度评测。现在进行进一步分析,解读编译器生成的MSIL(微软中间语言)代码。


一、前期准备

先找到“IL 反汇编程序”(开始\程序\Microsoft Visual Studio 2010\Microsoft Windows SDK Tools\)——

运行“IL 反汇编程序”,打开编译后的exe。展开节点,双击叶子节点查看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<class PointerCallAdd>(!!0, uint8*)class
基类泛型调用基类call       uint8*  CallClassPtr<class PointerCall>(!!0, uint8*)class
接口泛型调用派生类call       uint8* CallPtr<class  PointerCallAdd>(!!0, uint8*)class
接口泛型调用密封类call       uint8* CallPtr<class  SldPointerCallAdd>(!!0, uint8*)class
接口泛型调用结构体call       uint8*  CallPtr<valuetype SPointerCallAdd>(!!0, uint8*)valuetype
接口泛型调用结构体引用call       uint8*  CallRefPtr<valuetype SPointerCallAdd>(!!0&, uint8*)valuetype
接口泛型调用基类call       uint8* CallPtr<class  PointerCall>(!!0, uint8*)class
接口泛型调用派生类的接口call       uint8* CallPtr<class  IPointerCall>(!!0, uint8*)class
接口泛型调用密封类的接口call       uint8* CallPtr<class  IPointerCall>(!!0, uint8*)class
接口泛型调用结构体的接口call       uint8* CallPtr<class  IPointerCall>(!!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<valuetype TryPointerCall.SPointerCallAdd>(!!0,uint8*)


接口泛型调用结构体引用——
  IL_03dd:  ldloca.s   V_2
  IL_03df:  ldloc.s    V_7
  IL_03e1:  call       uint8* TryPointerCall.PointerCallTool::CallRefPtr<valuetype TryPointerCall.SPointerCallAdd>(!!0&,uint8*)

可见泛型调用的IL代码并不复杂,与普通调用基本一样,也是先将参数放入堆栈再call。对于引用参数,将“ldloc.*”指令换成“ldloca.s”指令就行了。

(完)

目录——
C#类与结构体究竟谁快——各种函数调用模式速度评测:http://blog.csdn.net/zyl910/article/details/6788417
再探C#类与结构体究竟谁快——考虑栈变量、栈分配、64位整数、密封类:http://blog.csdn.net/zyl910/article/details/6793908
三探C#类与结构体究竟谁快——MSIL(微软中间语言)解读:http://blog.csdn.net/zyl910/article/details/6817158
四探C#类与结构体究竟谁快——跨程序集(assembly)调用:http://blog.csdn.net/zyl910/article/details/6839868

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值