為什麽處理有序數組比無序數組快?
有興趣學習教流c/c++的小夥伴可以加群:941636044
問題
由於某些怪異的原因,下面這段C++代碼表現的異乎尋常—-當這段代碼作用於有序數據時其速度可以提高將近6倍,這真是令人驚奇。
#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語言寫了個例子,如下:
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
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
if(data[c] >=128)
sum += data[c];
}
}
System. out.println((System. nanoTime() - start) / 1000000000.0);
System. out.println( "sum = " + sum);
}
}
上述例子運行結果和前面C++例子運行的結果差異,雖然沒有C++中那麽大,但是也有幾分相似。
對於上面的問題,我首先想的原因是排序可能會導致數據有緩存,但是轉念一想之前原因有點不切實際,因為上面的數組都是剛剛生成的,所以我的問題是:
上述代碼運行時到底發生了什麽?
為什麽運行排好序的數組會比亂序數組快?
上述代碼求和都是獨立的,而順序不應該會產生影響。
回答
其實,你是分支預測(branch prediction )失敗的受害者。
什麽是分支預測?
考慮一個鐵路樞紐:
Imageby Mecanismo, via Wikimedia Commons. Used und
為了便於討論,假設現在是1800年,這時候還沒有出現遠程或廣播通訊工具。
你是一個鐵路樞紐的工人。當你聽到火車開來時,你不知道這個火車要走哪一條路,只有讓火車停下來詢問列車長火車要開往哪,最後你將軌道切換到相應的方向。
火車的質量非常大,固慣性很大,因此火車需要經常性的加速減速。
有沒有更好的方法喃?可以猜火車將行駛的方向應該是可行的!
如果猜對了,火車繼續往前走;
如果猜錯了,列車長會讓火車停下來,並後退,然後告訴你正確的方向,然後火車重新啟動開往正確的方向。
考慮一個if語句:在處理器級別上,他是一個分支指令:
有興趣交流學習c/c++的小夥伴可以加群:941636044
你來扮演處理器,當你遇到一個分支,你不知道它要走哪條路,該怎麽辦?你可以停止執行並等待直到之前的指令執行完。然後繼續執行正確路徑的指令。
有沒有更好的方法喃?可以猜測哪個分支將要被執行!
如果猜對了,繼續執行;
如果猜錯了,你需要刷新管道並且回退到該分支,重新啟動執行正確的方向。
如果每次都能猜對,整個執行過程就不會停止。
如果經常猜錯,就需要在停止、回退、重新執行上花費非常多的時間。
這就是分支預測。不得不承認這不是一個最好的比喻因為火車可以僅僅使用一個標誌表示其前進的方向。但是對於計算機,直到最後時刻,處理器是不知道哪條分支被執行。
想想可以使用什麽預測策略使得火車回退的次數最少?哈哈,可以利用歷史數據!如果火車100次有99次都是向左,那麽下次預測結果仍向左。如果過去數據是交替的,那麽預測結果也是交替的。如果它每3次都換一個方向,那麽預測也采用相同的方法。
簡而言之,你需要嘗試尋找出一個規則(模式)然後按照它進行預測就可以了。分支預測基本上就是這樣工作的。
大部分應用程序的分支是很規律的。這也是為什麽現代的分支預測的準確率基本上都在90%以上。但是當沒有規律、不可預測的分支時候,分支預測就顯得比較拙雞了。
關於分支預測更多詳細的內容可參閱:維基百科
從上面可以得到啟發,這個問題的“罪魁禍首”就是 if 語句
if (data[c] >= 128)
um += data[c];
註意到數據是在0到255均勻分布的。當排好序後,小於等於128的前半部分是不會執行if語句的,大於128的後半部分都會進入if語句。
這是非常有好的分支預測因為分支會連續多次執行相同的分支。即使是一個簡單的飽和計數器也會預測正確除去當變換方向後的少數幾個。
快速可視化
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)
然而,如果數據是完全隨機的,分支預測則毫無用處因為它不能預測隨機數據。這種情況下可能會有50%的錯誤預測。
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 ,如果能夠可以犧牲可讀性的表現。
將下面代碼
if (data[c] >= 128)
sum += data[c];
替換為:
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
// 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
// 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),並且和帶分支同樣快。
通過上面說明,即使比較成熟的現代編譯器在優化代碼的上可以有很大的不同。
最後,有興趣一起學習交流c/c++的小夥伴可以加群:941636044
為什麽處理有序數組比無序數組快?