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

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

如何证明

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

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

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

    原因有两个:

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

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

我的证明

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

类型定义
 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个重写了的值类型,并且已经将类名整理成相同的长度,避免不必要的误差。

    再加一个计时器:


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 

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

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)的性能基本相当(当然会有些误差,不过这些误差原没有装箱的代价大),
  • 证明不覆盖的情况下会导致装箱,
  • 证明在不正确覆盖的前提下,并不见得能提高性能
  • 证明在正确的覆盖的前提下,可以提高性能
  • 证明在不正确覆盖的前提下,某些条件下反而会降低性能
posted on 2011-03-29 22:00  Zhenway  阅读(656)  评论(7编辑  收藏  举报