论证:ValueType的重载的方法到底会不会导致装箱

    前几日,读了刀刀的一篇装箱拆箱 深度理解,刀刀认为由于ValueType中重写了ToString等方法,因此,在调用这些方法时,不会导致装箱,而我的观点正好相反,ValueType中重写的这些方法如果没有在值类型中重写,那么依然会被装箱。

    既然两个人都表达了自己的论点,那么,必须要拿出相应的证据,来证明各自的观点。

如何证明

    刀刀在回复中指出因为IL中没有使用box指令,因此不会发生装箱,不过这个论据并不能让我信服,原因很简单,IL中除了显式的box指令会导致装箱外,还有Constrained+虚方法调用形式(2.0为了支持泛型而加出来的Op),这种方式会导致隐式的装箱。

    既然IL不能证明,那么什么方式可以证明?

    大家还记得平时说的要避免频繁装箱不?为什么要尽量避免哪?

    原因有两个:

  • 装箱分配内存,影响性能
  • 导致很多临时性的对象,加重GC负担

    我们正可以利用第一点来证明到底有没有发生装箱。

我的证明

    首先,准备5个不同的类型:

ExpandedBlockStart.gif 类型定义
 1  public   class  ReferenceTypeByGetType__
 2  {
 3       public   override   string  ToString()
 4      {
 5           return  GetType().ToString();
 6      }
 7  }
 8 
 9  public   class  ReferenceTypeByTypeOf___
10  {
11       public   override   string  ToString()
12      {
13           return   typeof (ReferenceTypeByTypeOf___).ToString();
14      }
15  }
16 
17  public   struct  ValueTypeWithoutOverride { }
18 
19  public   struct  ValueTypeWithOverride___
20  {
21       public   override   string  ToString()
22      {
23           return   this .GetType().ToString();
24      }
25  }
26 
27  public   struct  ValueTypeWithOverrideFix
28  {
29       public   override   string  ToString()
30      {
31           return   typeof (ValueTypeWithOverrideFix).ToString();
32      }
33 
    这5个类型分别为,2个引用类型(用于对比GetType与typeof的性能差异),1个没有重写的值类型(没有重写的话,实际实现就是GetType().ToString()),2个重写了的值类型,并且已经将类名整理成相同的长度,避免不必要的误差。

    再加一个计时器:


ExpandedBlockStart.gif MeasureCost
1  static   void  MeasureCost < T > ( string  title, Action < T >  action, T args)
2  {
3      var begin  =  Stopwatch.GetTimestamp();
4      action(args);
5      var end  =  Stopwatch.GetTimestamp();
6      Console.WriteLine(title  +   "  Cost:  "   +  (( double )(end  -  begin)  /  Stopwatch.Frequency).ToString( " f6 " +   " s " );
7 

    然后,就可以写比较代码了:

 
  
ExpandedBlockStart.gif Measures
 1  private   static   void  MeasureToString()
 2  {
 3      MeasureCost( " ReferenceTypeByGetType " , x  =>
 4      {
 5           string  s  =   null ;
 6           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
 7              s  =  x.ToString();
 8          Console.WriteLine(s);
 9      },  new  ReferenceTypeByGetType__());
10      MeasureCost( " ReferenceTypeByTypeOf " , x  =>
11      {
12           string  s  =   null ;
13           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
14              s  =  x.ToString();
15          Console.WriteLine(s);
16      },  new  ReferenceTypeByTypeOf___());
17      MeasureCost( " ValueTypeWithoutOverride " , x  =>
18      {
19           string  s  =   null ;
20           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
21              s  =  x.ToString();
22          Console.WriteLine(s);
23      },  new  ValueTypeWithoutOverride());
24      MeasureCost( " ValueTypeWithOverride " , x  =>
25      {
26           string  s  =   null ;
27           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
28              s  =  x.ToString();
29          Console.WriteLine(s);
30      },  new  ValueTypeWithOverride___());
31      MeasureCost( " ValueTypeWithOverrideFix " , x  =>
32      {
33           string  s  =   null ;
34           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
35              s  =  x.ToString();
36          Console.WriteLine(s);
37      },  new  ValueTypeWithOverrideFix());
38      MeasureCost( " ValueTypeWithOverrideFix (boxing manual) " , x  =>
39      {
40           string  s  =   null ;
41           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
42              s  =  (( object )x).ToString();
43          Console.WriteLine(s);
44      },  new  ValueTypeWithOverrideFix());
45      MeasureCost( " Just a string " , x  =>
46      {
47           string  s  =   null ;
48           for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
49              s  =  x.ToString();
50          Console.WriteLine(s);
51      },  " Just a string. " );
52 

  最后,添加一个字符串,字符串的ToString就是返回自身,用于比较循环本身的代价,对了,差点忘了这个:

const   int  LoopCount  =   10000000 ;

    这样,测试代码就准备好了,来看看Release下的执行结果吧:

starting MeasureToString...
ConsoleApplication10.ReferenceTypeByGetType__
ReferenceTypeByGetType Cost: 0.154173s
ConsoleApplication10.ReferenceTypeByTypeOf___
ReferenceTypeByTypeOf Cost: 0.154043s
ConsoleApplication10.ValueTypeWithoutOverride
ValueTypeWithoutOverride Cost: 0.223557s
ConsoleApplication10.ValueTypeWithOverride___
ValueTypeWithOverride Cost: 0.217400s
ConsoleApplication10.ValueTypeWithOverrideFix
ValueTypeWithOverrideFix Cost: 0.149262s
ConsoleApplication10.ValueTypeWithOverrideFix
ValueTypeWithOverrideFix (boxing manual) Cost: 0.199377s
Just a string.
Just a string Cost: 0.024276s

分析

    跑出结果并不难,问题是要正确分析。

    首先,看第一个和第二个,比较GetType().ToString()和typeof(xxx).ToString()的性能差异,,当然试验结果是非常接近的,因此,我们可以认为对于引用类型而言GetType()和typeof(xxx)的性能是非常接近的。

    然后,看第三个和第四个,一个是ValueType的默认实现,另一个是override调用GetType().ToString()的实现,两者的性能也基本一致,别急着下结论说因此证明确实没有装箱。

    接着,看第四个和第五个,从第一个和第二个的比较中,已经可以证明,GetType()与typeof(xxx)的性能基本一致,但是在第四个和第五个的比较中,却看到了巨大的差异,为什么?很简单,GetType()是object的方法,因此每次调用都会被装箱,因此,第三个和第四个的性能才会如此接近,同时也证明第4个类的重写方式其实是错误的(至少在提高性能方面)。

    然后,将看一下第三、四个和第六个,第六个强制装箱后,性能与不重写,和错误的重写的性能基本一致,从而,证明不重写也会装箱。

    最后,Just a string.是用于计算循环本身和调用的方法的代价。

抛出个问题:为什么说第四个类的重写方式是错误的?

    不知道,大家有没有想过这个问题,值类型的某个方法里面调用了另一个导致自身装箱的方法。

    在前面的测试中,已经可以看出这个方式和不重写的性能基本一样

    不妨在做个试验:


1  MeasureCost( " ValueTypeWithOverride (boxing manual) " , x  =>
2  {
3       string  s  =   null ;
4       for  ( int  i  =   0 ; i  <  LoopCount; i ++ )
5          s  =  (( object )x).ToString();
6      Console.WriteLine(s);
7  },  new  ValueTypeWithOverride___());

    再看看执行结果:

ConsoleApplication10.Program
starting MeasureToString...
ConsoleApplication10.ReferenceTypeByGetType__
ReferenceTypeByGetType Cost: 0.157408s
ConsoleApplication10.ReferenceTypeByTypeOf___
ReferenceTypeByTypeOf Cost: 0.148192s
ConsoleApplication10.ValueTypeWithoutOverride
ValueTypeWithoutOverride Cost: 0.216962s
ConsoleApplication10.ValueTypeWithOverride___
ValueTypeWithOverride Cost: 0.213888s
ConsoleApplication10.ValueTypeWithOverrideFix
ValueTypeWithOverrideFix Cost: 0.149800s
ConsoleApplication10.ValueTypeWithOverrideFix
ValueTypeWithOverrideFix (boxing manual) Cost: 0.202390s
Just a string.
Just a string Cost: 0.020379s
ConsoleApplication10.ValueTypeWithOverride___
ValueTypeWithOverride (boxing manual) Cost: 0.271406s

    这个错误的重载方式,在手工装箱的情况下,跑出来的成绩进一步落后了0.06s(基本就是装箱需要的时间),也就是说,这样的错误重载,在极端情况下可能导致两次装箱。

梳理结论

    重新梳理一下整个过程,可以得出下列结论:

  • 使用引用类型类证明GetType()和typeof(xxx)的性能基本相当(当然会有些误差,不过这些误差原没有装箱的代价大),
  • 证明不覆盖的情况下会导致装箱,
  • 证明在不正确覆盖的前提下,并不见得能提高性能
  • 证明在正确的覆盖的前提下,可以提高性能
  • 证明在不正确覆盖的前提下,某些条件下反而会降低性能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值