zoukankan      html  css  js  c++  java
  • 享受release版本发布的好处的同时也应该警惕release可能给你引入一些莫名其妙的大bug

      一般我们发布项目的时候通常都会采用release版本,因为release会在jit层面对我们的il代码进行了优化,比如在迭代和内存操作的性能提升方面,废话不多说,

    我先用一个简单的“冒泡排序”体验下release和debug下面的性能差距。

    一:release带来的闪光点【冒泡排序】

      这个是我多年前写的算法系列中的一个冒泡排序的例子,就随手翻出来展示一下,准备灌入50000条数据,这样就可以执行25亿次迭代,王健林说,不能太张

    狂,几十亿对我来说不算小意思,算中等意思吧。

     1 namespace ConsoleApplication4
     2 {
     3     class Program
     4     {
     5         static void Main(string[] args)
     6         {
     7             var rand = new Random();
     8             List<int> list = new List<int>();
     9 
    10             for (int i = 0; i < 50000; i++)
    11             {
    12                 list.Add(rand.Next());
    13             }
    14 
    15             var watch = Stopwatch.StartNew();
    16 
    17             try
    18             {
    19                 BubbleSort(list);
    20             }
    21             catch (Exception ex)
    22             {
    23                 Console.WriteLine(ex.Message);
    24             }
    25 
    26             watch.Stop();
    27 
    28             Console.WriteLine("耗费时间:{0}", watch.Elapsed);
    29         }
    30 
    31         //冒泡排序算法
    32         static List<int> BubbleSort(List<int> list)
    33         {
    34             int temp;
    35             //第一层循环: 表明要比较的次数,比如list.count个数,肯定要比较count-1次
    36             for (int i = 0; i < list.Count - 1; i++)
    37             {
    38                 //list.count-1:取数据最后一个数下标,
    39                 //j>i: 从后往前的的下标一定大于从前往后的下标,否则就超越了。
    40                 for (int j = list.Count - 1; j > i; j--)
    41                 {
    42                     //如果前面一个数大于后面一个数则交换
    43                     if (list[j - 1] > list[j])
    44                     {
    45                         temp = list[j - 1];
    46                         list[j - 1] = list[j];
    47                         list[j] = temp;
    48                     }
    49                 }
    50             }
    51             return list;
    52         }
    53     }
    54 }

    Debug下面的执行效率:

    Release下面的执行效率:

    从上面两张图可以看到,debug和release版本之间的性能差异能达到将近4倍的差距。。。还是相当震撼的。

    二:release应该注意的bug

      release确实是一个非常好的东西,但是在享受好处的同时也不要忘了,任何优化都是要付出代价的,这世界不会什么好事都让你给占了,release有时候为了

    性能提升,会大胆的给你做一些代码优化和cpu指令的优化,比如说把你的一些变量和参数缓存在cpu的高速缓存中,不然的话,你的性能能提升这么多么~~~

    绝大多数情况下都不会遇到问题,但有时你很不幸,要出就出大问题,下面我同样举一个例子给大家演示一下:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             var isStop = false;
     6 
     7             var task = Task.Factory.StartNew(() =>
     8             {
     9                 var isSuccess = false;
    10 
    11                 while (!isStop)
    12                 {
    13                     isSuccess = !isSuccess;
    14                 }
    15             });
    16 
    17             Thread.Sleep(1000);
    18             isStop = true;
    19             task.Wait();
    20 
    21             Console.WriteLine("主线程执行结束!");
    22             Console.ReadLine();
    23         }
    24     }

    上面这串代码的意思很简单,我就不费劲给大家解释了,但是有意思的事情就是,这段代码在debug和release的环境下执行的结果却是天壤之别,而我们的常规

    思想其实就是1ms之后,主线程执行console.writeline(...)对吧,而真相却是:debug正常输出,release却长久卡顿。。。。一直wait啦。。。。这是一个大

    bug啊。。。不信的话你可以看看下面的截图嘛。。。

    debug:

    release:

    三:问题猜测

    刚才也说过了,release版本会在jit层面对il代码进行优化,所以看应用程序的il代码是看不出什么名堂的,但是可以大概能猜到的就是,要么jit直接把代码

    1 while (!isStop)
    2 {
    3       isSuccess = !isSuccess;
    4  }

    优化成了

    1 while (true)
    2 {
    3      isSuccess = !isSuccess;
    4 }

    要么就是为了加快执行速度,mainthread和task会将isStop变量从memory中加载到各自的cpu缓存中,而主线程执行isStop=true的时候而task读的还是cpu

    缓存中的脏数据,也就是还是按照isStop=false的情况进行执行。

    四:三种解决方案

    1:volatile 

    那这个问题该怎么解决呢?大家第一个想到的就是volatile关键词,这个关键词我想大家都知道有2个意思:

    <1>. 告诉编译器,jit,cpu不要对我进行任何形式的优化,谢谢。

    <2>. 该变量必须从memory中读取,而不是cpu cache中。

    所以可以将上面的代码优化成如下方式,问题就可以完美解决:

     1    class Program
     2     {
     3         volatile static bool isStop = false;
     4 
     5         static void Main(string[] args)
     6         {
     7             var task = Task.Factory.StartNew(() =>
     8             {
     9                 var isSuccess = false;
    10 
    11                 while (!isStop)
    12                 {
    13                     isSuccess = !isSuccess;
    14                 }
    15             });
    16 
    17             Thread.Sleep(1000);
    18             isStop = true;
    19             task.Wait();
    20 
    21             Console.WriteLine("主线程执行结束!");
    22             Console.ReadLine();
    23         }
    24     }

    2:Thread.VolatileRead

      这个方法也是.net后来新增的一个方法,它的作用就是告诉CLR,我需要从memory中进行读取,而不是cpu cache中,不信可以看下注释。

     1         //
     2         // 摘要:
     3         //     读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。
     4         //
     5         // 参数:
     6         //   address:
     7         //     要读取的字段。
     8         //
     9         // 返回结果:
    10         //     由任何处理器写入字段的最新值。
    11         public static byte VolatileRead(ref byte address);

    不过很遗憾,这吊毛没有bool类型的参数,只有int类型。。。操,,,为了测试只能将isStop改成0,1这两种int状态,哎。。。

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             int isStop = 0;
     6 
     7             var task = Task.Factory.StartNew(() =>
     8             {
     9                 var isSuccess = false;
    10 
    11                 while (isStop != 1)
    12                 {
    13                     //每次循环都要从内存中读取 ”isStop“ 的最新值
    14                     Thread.VolatileRead(ref isStop);
    15 
    16                     isSuccess = !isSuccess;
    17                 }
    18             });
    19 
    20             Thread.Sleep(1000);
    21             isStop = 1;
    22             task.Wait();
    23 
    24             Console.WriteLine("主线程执行结束!");
    25             Console.ReadLine();
    26         }
    27     }

    3: Thread.MemoryBarrier

      其实这个方法在MSDN上的解释看起来让人觉得莫名奇妙,根本就看不懂。

    1         //
    2         // 摘要:
    3         //     按如下方式同步内存存取:执行当前线程的处理器在对指令重新排序时,不能采用先执行 System.Threading.Thread.MemoryBarrier
    4         //     调用之后的内存存取,再执行 System.Threading.Thread.MemoryBarrier 调用之前的内存存取的方式。
    5         [SecuritySafeCritical]
    6         public static void MemoryBarrier();

    其实这句话大概就两个意思:

    <1>. 优化cpu指令排序。

    <2>. 调用MemoryBarrier之后,在MemoryBarrier之前的变量写入都要从cache更新到memory中。

            调用MemoryBarrier之后,在MemroyBarrier之后的变量读取都要从memory中读取,而不是cpu cache中。

    所以基于上面两条策略,我们可以用Thread.MemoryBarrier进行改造,代码如下:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             bool isStop = false;
     6 
     7             var task = Task.Factory.StartNew(() =>
     8             {
     9                 var isSuccess = false;
    10 
    11                 while (!isStop)
    12                 {
    13                     Thread.MemoryBarrier();
    14                     isSuccess = !isSuccess;
    15                 }
    16             });
    17 
    18             Thread.Sleep(1000);
    19             isStop = true;
    20             task.Wait();
    21 
    22             Console.WriteLine("主线程执行结束!");
    23             Console.ReadLine();
    24         }
    25     }

    总结一下,在多线程环境下,多个线程对一个共享变量进行读写是一个很危险的操作,原因我想大家都明白了,这个时候你就可以用到上面三种手段进行解决

    啦。。。好了,本篇就说到这里,希望对你有帮助。

  • 相关阅读:
    [redis] 普通 RedisPool 的 CRUD 实现
    [maven] 常用仓库地址
    [saiku] 通过 saiku 的 DEMO 分析 connection
    [backbone]backbone.js
    [saiku] JCR在saiku中的运用原理
    [redis] 分布式 Redis 的 CRUD 实现
    [redis] session 保存到 redis 简单实现
    [redis] redis 存取键值对常用的三种使用方式
    226. Invert Binary Tree
    225. Implement Stack using Queues
  • 原文地址:https://www.cnblogs.com/huangxincheng/p/6585907.html
Copyright © 2011-2022 走看看