享受release版本釋出的好處的同時也應該警惕release可能給你引入一些莫名其妙的大bug

一線碼農發表於2017-03-20

  一般我們釋出專案的時候通常都會採用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     }

 

總結一下,在多執行緒環境下,多個執行緒對一個共享變數進行讀寫是一個很危險的操作,原因我想大家都明白了,這個時候你就可以用到上面三種手段進行解決

啦。。。好了,本篇就說到這裡,希望對你有幫助。

 

相關文章