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


        前几日,读了刀刀的一篇装箱拆箱 深度理解,刀刀认为由于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)的性能基本相当(当然会有些误差,不过这些误差原没有装箱的代价大),
    • 证明不覆盖的情况下会导致装箱,
    • 证明在不正确覆盖的前提下,并不见得能提高性能
    • 证明在正确的覆盖的前提下,可以提高性能
    • 证明在不正确覆盖的前提下,某些条件下反而会降低性能
  • 相关阅读:
    如何在ASP.NET 5和XUnit.NET中进行LocalDB集成测试
    如何在单元测试过程中模拟日期和时间
    Azure Blob Storage从入门到精通
    免费电子书:使用VS Online敏捷管理开源项目
    使用ASP.NET 5开发AngularJS应用
    Syncfusion的社区许可及免费电子书和白皮书
    理解ASP.NET 5的中间件
    惊鸿一瞥(Glimpse)——开发之时即可掌控ASP.NET应用的性能
    用于Simple.Data的ASP.NET Identity Provider
    大数据技术之_19_Spark学习_01_Spark 基础解析小结(无图片)
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1999215.html
Copyright © 2020-2023  润新知