爲何處理有序數組比無序數組快?

聲明:本文轉自 伯樂在線 - Jerry ,原文地址:http://blog.jobbole.com/68023/html

因爲某些怪異的緣由,下面這段C++代碼表現的異乎尋常—-當這段代碼做用於有序數據時其速度能夠提升將近6倍,這真是使人驚奇。java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <algorithm>
#include <ctime>
#include <iostream>
 
int _tmain ( int argc , _TCHAR * argv [])
{
         //Generate data
         const unsigned arraySize = 32768;
         int data[arraySize];
 
         for ( unsigned c = 0; c < arraySize; ++c)
               data[c] = std:: rand () % 256;
 
         //!!! With this, the next loop runs faster
        std::sort(data, data + arraySize);
 
         //Test
         clock_t start = clock ();
         long long sum = 0;
         for ( unsigned i = 0; i < 100000; ++i){
                //Primary loop
                for ( unsigned c = 0; c < arraySize; ++c){
                       if (data[c] >= 128)
                            sum += data[c];
               }
        }
         double eclapsedTime = static_cast < double >( clock () - start) / CLOCKS_PER_SEC;
 
        std::cout << eclapsedTime << std::endl;
        std::cout << "sum = " << sum << std::endl;
         return 0;
}
  • 若是把 std::sort(data, data+arraySize) 去掉,這段代碼耗時11.54秒。
  • 對於有序數據,這段代碼耗時1.93秒

起初我覺得這多是某一種語言或某一個編譯器發生的異常的事件,後來我在java語言寫了個例子,以下:ios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.Arrays;
import java.util.Random;
 
public class Test_Sorted_UnSorted_Array {
        public static void main(String[] args) {
              //Generate data
              int arraySize = 32768 ;
              int data[] = new int [arraySize];
 
             Random rnd = new Random( 0 );
              for ( int c = 0 ; c<arraySize; ++c)
                   data[c] = rnd.nextInt()% 256 ;
 
              //!!! With this, the next loop runs faster
             Arrays. sort(data);
 
              //Test
              long start = System. nanoTime();
              long sum = 0 ;
 
              for ( int i= 0 ; i< 100000 ; ++i){
                    //Primary loop
                    for ( int c = 0 ; c<arraySize; ++c){
                          if (data[c] >= 128 )
                               sum += data[c];
                   }
             }
 
             System. out.println((System. nanoTime() - start) / 1000000000.0 );
             System. out.println( "sum = " + sum);
       }
}

上述例子運行結果和前面C++例子運行的結果差別,雖然沒有C++中那麼大,可是也有幾分類似。數組

對於上面的問題,我首先想的緣由是排序可能會致使數據有緩存,可是轉念一想以前緣由有點不切實際,由於上面的數組都是剛剛生成的,因此個人問題是:緩存

  • 上述代碼運行時到底發生了什麼?
  • 爲何運行排好序的數組會比亂序數組快?
  • 上述代碼求和都是獨立的,而順序不該該會產生影響。

 

來自 Mysticial 的最佳回覆

你是分支預測(branch prediction )失敗的受害者。less

什麼是分支預測?

考慮一個鐵路樞紐:dom

trains

Imageby Mecanismo, via Wikimedia Commons. Used under theCC-By-SA 3.0license.工具

 

爲了便於討論,假設如今是1800年,這時候尚未出現遠程或廣播通信工具。oop

你是一個鐵路樞紐的工人。當你聽到火車開來時,你不知道這個火車要走哪一條路,只有讓火車停下來詢問列車長火車要開往哪,最後你將軌道切換到相應的方向。測試

火車的質量很是大,固慣性很大,所以火車須要常常性的加速減速。

有沒有更好的方法喃?能夠猜火車將行駛的方向應該是可行的!

  • 若是猜對了,火車繼續往前走;
  • 若是猜錯了,列車長會讓火車停下來,並後退,而後告訴你正確的方向,而後火車從新啓動開往正確的方向。

考慮一個if語句:在處理器級別上,他是一個分支指令:

if

你來扮演處理器,當你遇到一個分支,你不知道它要走哪條路,該怎麼辦?你能夠中止執行並等待直到以前的指令執行完。而後繼續執行正確路徑的指令。

有沒有更好的方法喃?能夠猜想哪一個分支將要被執行!

  • 若是猜對了,繼續執行;
  • 若是猜錯了,你須要刷新管道而且回退到該分支,從新啓動執行正確的方向。

若是每次都能猜對,整個執行過程就不會中止。
若是常常猜錯,就須要在中止、回退、從新執行上花費很是多的時間。

這就是分支預測。不得不認可這不是一個最好的比喻由於火車能夠僅僅使用一個標誌表示其前進的方向。可是對於計算機,直到最後時刻,處理器是不知道哪條分支被執行。

想一想可使用什麼預測策略使得火車回退的次數最少?哈哈,能夠利用歷史數據!若是火車100次有99次都是向左,那麼下次預測結果仍向左。若是過去數據是交替的,那麼預測結果也是交替的。若是它每3次都換一個方向,那麼預測也採用相同的方法。

簡而言之,你須要嘗試尋找出一個規則(模式)而後按照它進行預測就能夠了。分支預測基本上就是這樣工做的。
大部分應用程序的分支是很規律的。這也是爲何現代的分支預測的準確率基本上都在90%以上。可是當沒有規律、不可預測的分支時候,分支預測就顯得比較拙雞了。

關於分支預測更多詳細的內容可參閱:維基百科


從上面能夠獲得啓發,這個問題的「罪魁禍首」就是 if 語句

1
2
if (data[c] >= 128)
     um += data[c];

注意到數據是在0到255均勻分佈的。當排好序後,小於等於128的前半部分是不會執行if語句的,大於128的後半部分都會進入if語句。

這是很是有好的分支預測由於分支會連續屢次執行相同的分支。即便是一個簡單的飽和計數器也會預測正確除去當變換方向後的少數幾個。

快速可視化

1
2
3
4
5
6
7
T = branch taken
N = branch not taken
 
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...
 
        = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)</span></code>

然而,若是數據是徹底隨機的,分支預測則毫無用處由於它不能預測隨機數據。這種狀況下可能會有50%的錯誤預測。

1
2
3
data[]= 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch=  T,   T,   N,   T,   T,   T,   T,   N,  T,   N,   N,  T,   T,   T,   N ...
       = TTNTTTTNTNNTTTN ... (completely random - hard to predict)

那這種狀況下該怎麼作呢?

若是編譯器不能將分支優化爲有條件的移動,這時候能夠嘗試一些 Hacks ,若是可以能夠犧牲可讀性的表現。
將下面代碼

1
2
if (data[c] >= 128)
     sum += data[c];

替換爲:

1
2
int t = (data[c] - 128) >> 31;
     sum += ~t & data[c];

用一些按位操做取代分支判斷,這樣就去除了分支。(注意:這個 hacks 並非和if語句嚴格相等,可是在咱們這個例子裏,對輸入數組data的全部值都是正確的)

Benchmarks: Core i7 920 @ 3.5 GHz
C++ – Visual Studio 2010 – x64 Release

1
2
3
4
5
6
7
8
9
10
11
//  Branch - Random
seconds = 11.777
 
//  Branch - Sorted
seconds = 2.352
 
//  Branchless - Random
seconds = 2.564
 
//  Branchless - Sorted
seconds = 2.587</span></code>

Java – Netbeans 7.1.1 JDK 7 – x64

1
2
3
4
5
6
7
8
9
10
11
//  Branch - Random
seconds = 10.93293813
 
//  Branch - Sorted
seconds = 5.643797077
 
//  Branchless - Random
seconds = 3.113581453
 
//  Branchless - Sorted
seconds = 3.186068823</span></code>

觀察可得:

  • 在分支狀況下:排序數組和亂序數組之間的結果有着巨大的差別。
  • 在 Hack 方式下:對於排序和亂序的結果則沒有差別。
  • 在C++中,對於排序數組,Hack 會比分支有一點點慢。

通常的經驗法則是避免數據依賴分支在一些特殊的循環中。

64位機器下,GCC 4.6.1附帶選項-O3或者-ftree-vectorize能夠產生一個條件移動。所以對於有序和亂序數據都是同樣快。

VC++2010不可以產生條件移動對於這樣的分支。

英特爾編譯器11一樣能夠作一些神奇的事。它經過互換兩個循環,從而提高了不可預測的分支外循環。所以,它不但可以避免誤預測,並且速度上能夠達到VC++和GCC的兩個快。換句話說,ICC利用了測試迴路打破了benchmark。

若是用英特爾編譯器執行沒有分支的代碼,它僅僅出右向量化(out-right vectorizes it),而且和帶分支一樣快。

經過上面說明,即便比較成熟的現代編譯器在優化代碼的上能夠有很大的不一樣。

 

【版權聲明】轉載請註明出處:http://www.cnblogs.com/TenosDoIt/p/3979538.html

相關文章
相關標籤/搜索