這世上有三樣東西是別人搶不走的:一是吃進胃裏的食物,二是藏在心中的夢想,三是讀進大腦的書java
如下是c++的一段很是神奇的代碼。因爲一些奇怪緣由,對數據排序後奇蹟般的讓這段代碼快了近6倍!!ios
#include <algorithm> #include <ctime> #include <iostream> int main() { // 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 elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC; std::cout << elapsedTime << std::endl; std::cout << "sum = " << sum << std::endl; }
std::sort(data, data + arraySize);
,這段代碼運行了11.54秒.如下是Java代碼段c++
import java.util.Arrays; import java.util.Random; public class Main { 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); } }
結果類似,沒有很大的差異。git
我首先得想法是排序把數據放到了cache中,可是我下一個想法是我以前的想法是多麼傻啊,由於這個數組剛剛被構造。github
看看這個鐵路分岔口
Image by Mecanismo, via Wikimedia Commons. Used under the CC-By-SA 3.0 license.數組
爲了理解這個問題,想象一下,若是咱們回到19世紀.less
你是在分岔口的操做員。當你聽到列車來了,你沒辦法知道這兩條路哪一條是正確的。而後呢,你讓列車停下來,問列車員哪條路是對的,而後你才轉換鐵路方向。dom
火車很重有很大的慣性。因此他們得花費很長的時間開車和減速。oop
是否是有個更好的辦法呢?你猜想哪一個是火車正確的行駛方向性能
若是你每次都猜對了,那麼火車永遠不會停下來。
若是你猜錯太屢次,那麼火車會花費不少時間來停車,返回,而後再啓動
考慮一個if條件語句:在處理器層面上,這是一個分支指令:
當處理器看到這個分支時,沒辦法知道哪一個將是下一條指令。該怎麼辦呢?貌似只能暫停執行,直到前面的指令完成,而後再繼續執行正確的下一條指令?
現代處理器很複雜,所以它須要很長的時間"熱身"、"冷卻"
是否是有個更好的辦法呢?你猜想下一個指令在哪!
若是你每次都猜對了,那麼你永遠不會停
若是你猜錯了太屢次,你就要花不少時間來滾回,重啓。
這就是分支預測。我認可這不是一個好的類比,由於火車能夠用旗幟來做爲方向的標識。可是在電腦中,處理器不能知道哪個分支將走到最後。
因此怎樣能很好的預測,儘量地使火車必須返回的次數變小?你看看火車以前的選擇過程,若是這個火車往左的機率是99%。那麼你猜左,反之亦然。若是每3次會有1次走這條路,那麼你也按這個三分之一的規律進行。
換句話說,你試着定下一個模式,而後按照這個模式去執行。這就差很少是分支預測是怎麼工做的。
大多數的應用都有很好的分支預測。因此現代的分支預測器一般能實現大於90%的命中率。可是當面對沒有模式識別、沒法預測的分支,那分支預測基本就沒用了。
若是你想知道更多:Branch predictor" article on Wikipedia.
if (data[c] >= 128) sum += data[c];
注意到數據是分佈在0到255之間的。當數據排好序後,基本上前一半大的的數據不會進入這個條件語句,然後一半的數據,會進入該條件語句.
連續的進入同一個執行分支不少次,這對分支預測是很是友好的。能夠更準確地預測,從而帶來更高的執行效率。
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)
若是編譯器沒法優化帶條件的分支,若是你願意犧牲代碼的可讀性換來更好的性能的話,你能夠用下面的一些技巧。
把
if (data[c] >= 128) sum += data[c];
替換成
int t = (data[c] - 128) >> 31; sum += ~t & data[c];
這消滅了分支,把它替換成按位操做.
(說明:這個技巧不是很是嚴格的等同於原來的if條件語句。可是在data[]
當前這些值下是OK的)
使用的設備參數是: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
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
結論:
通常的建議是儘可能避免在關鍵循環上出現對數據很依賴的分支。(就像這個例子)
更新:
-O3
or -ftree-vectorize
,在64位機器上,數據有沒有排序,都是同樣快。說明了現代編譯器愈加成熟強大,能夠在這方面充分優化代碼的執行效率
CPU的流水線指令執行
想象如今有一堆指令等待CPU去執行,那麼CPU是如何執行的呢?具體的細節能夠找一本計算機組成原理來看。CPU執行一堆指令時,並非單純地一條一條取出來執行,而是按照一種流水線的方式,在CPU真正指令前,這條指令就像工廠裏流水線生產的產品同樣,已經被通過一些處理。簡單來講,一條指令可能通過過程:取指(Fetch)、解碼(Decode)、執行(Execute)、放回(Write-back)。
假設如今有指令序列ABCDEFG。當CPU正在執行(execute)指令A時,CPU的其餘處理單元(CPU是由若干部件構成的)其實已經預先處理到了指令A後面的指令,例如B可能已經被解碼,C已經被取指。這就是流水線執行,這能夠保證CPU高效地執行指令。
分支預測
如上所說,CPU在執行一堆順序執行的指令時,由於對於執行指令的部件來講,其基本不須要等待,由於諸如取指、解碼這些過程早就被作了。可是,當CPU面臨非順序執行的指令序列時,例如以前提到的跳轉指令,狀況會怎樣呢?
取指、解碼這些CPU單元並不知道程序流程會跳轉,只有當CPU執行到跳轉指令自己時,才知道該不應跳轉。因此,取指解碼這些單元就會繼續取跳轉指令以後的指令。當CPU執行到跳轉指令時,若是真的發生了跳轉,那麼以前的預處理(取指、解碼)就白作了。這個時候,CPU得從跳轉目標處臨時取指、解碼,而後纔開始執行,這意味着:CPU停了若干個時鐘週期!
這實際上是個問題,若是CPU的設計聽任這個問題,那麼其速度就很難提高起來。爲此,人們發明了一種技術,稱爲branch prediction,也就是分支預測。分支預測的做用,就是預測某個跳轉指令是否會跳轉。而CPU就根據本身的預測到目標地址取指令。這樣,便可從必定程度提升運行速度。固然,分支預測在實現上有不少方法。
stackoverflow連接:
這個問題的全部回答中,最高的回答,獲取了上萬個vote,還有不少個回答,很是瘋狂,你們以爲不過癮能夠移步到這裏查看
http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array