語句的效能取決於它的判斷條件是否擁有可預測模式,你知道嗎?如果條件始終為真或者始終為假,處理器內的分支預測邏輯會選擇該模式。此外,如果該模式不可預測,if語句的開銷將更大。本文將解釋為什麼現代處理器要這樣做。
接下來,我們在不同條件下測試該迴圈的效能:
1 |
for (int i = 0; i < max; i++) if (<condition>) sum++; |
以下是不同真-假模式下該迴圈的耗時:
條件 | Pattern/模式 | 時間 (ms) |
(i & 0x80000000) == 0 | T repeated | 322 |
(i & 0xffffffff) == 0 | F repeated | 276 |
(i & 1) == 0 | TF alternating | 760 |
(i & 3) == 0 | TFFFTFFF… | 513 |
(i & 2) == 0 | TTFFTTFF… | 1675 |
(i & 4) == 0 | TTTTFFFFTTTTFFFF… | 1275 |
(i & 8) == 0 | 8T 8F 8T 8F … | 752 |
(i & 16) == 0 | 16T 16F 16T 16F … | 490 |
語句在“壞”真-假模式下比在“好”模式下慢了近六倍!當然,模式的好壞取決於編譯器產生的具體指令集和具體處理器。
讓我們看看處理器計數器
瞭解處理器如何利用時間的一種方式是檢視硬體計數器。為幫助效能除錯,現代處理器在執行程式碼時跟蹤各種計數器:執行指令的數量,各種型別記憶體訪問的數量,遇到分支的數量等等。要讀取計數器,你需要像Visual Studio 2010預覽版或旗艦版中的profiler,AMD Code Analyst或者Intel VTune類似的工具。
為了驗證我們觀察到的效能降低是由於if語句造成的,我們可以檢視分支預測錯誤計數器:
最糟糕的模式(TTFFTTFF…)將導致744次分支預測錯誤,而好的模式下大約只有10次。無疑壞情況花費最長的1.67秒,而好模式下僅花費大約300毫秒!
接下來分析分支預測做了什麼,以及為什麼它會對處理器效能有這麼大影響。
分支預測扮演什麼角色?
為解釋分支預測是什麼以及為何會影響效能引數,首先我們需要了解一下現代處理器的工作原理。為了執行每條指令,CPU會經歷以下這些(可能更多)步驟:
1. 取指:讀取下一條指令。
2. 譯碼:完成指令的翻譯。
3. 執行:執行指令。
4. 回寫:儲存結果到記憶體。
一個重要的優化是流水線階段可同時處理不同指令。那麼,在讀取一條指令時,第二條指令正在譯碼,第三條指令正在執行,而第四條指令正在回寫。現代處理器有10-31級流水線(例如,Pentium 4 Prescott有31級),為優化效能,保持所有階段儘可能一直工作非常重要。
影像來源於http://commons.wikimedia.org/wiki/File:Pipeline,_4_stage.svg
分支(即條件跳轉)給處理器流水線提出一個難題。在獲取一條指令後,處理器需要獲取下一條指令。但是下一條指令有兩種可能!處理器不確定取哪一條,直到分支條件指令執行到流水線結束。
與暫停流水線直至分支條件指令完全執行不同,現代處理器嘗試預測是否需要進行跳轉。然後處理器會讀取它認為正確的下一條指令。如果預測出錯,處理器會丟棄在流水線上執行了部分的指令。在維基分支預測器實現頁面上,可以看到處理器獲取並解釋分支統計資料的一些經典技術。
現代分支預測器適合分析簡單模式:全真,全假,真-假交替等。但是如果模式超出分支預測器的預測,效能影響將會非常嚴重。幸好大部分分支很容易預測,比如下面兩個高亮的例子:
1 2 3 4 5 6 7 |
int SumArray(int[] array) { if (array == null) throw new ArgumentNullException("array"); int sum=0; for(int i=0; i<array.Length; i++) sum += array[i]; return sum; } |
第一個高亮的條件檢查輸入的合法性,那麼該分支很少會執行。第二條高亮條件是迴圈終止條件。這也幾乎總是朝著一個方向走,除非處理的陣列非常短。所以,在這些情況下,與大多數情況類似,處理器分支預測邏輯可以有效防止處理器流水線暫停。
更新和說明
本文被reddit收錄,並且在reddit評論中獲得不少關注。我會對下面的問題、評論和批評進行回覆。
首先,關於分支預測優化通常是個壞主意的評論:我同意。我沒有在文中任何地方爭辯說你應該嘗試為分支預測優化你的程式碼。對於絕大部分高階語言程式碼,我甚至不敢想象你們是如何做到的。
第二點,有人擔心除常量值,是不是不同情況下執行的指令都不一樣。它們是一樣的——我檢視了JIT-ted彙編。如果你想要了解JIT-ted彙編程式碼或者C#原始碼,請給我發封郵件,我會把他們傳送給你。(我在這裡不貼出程式碼,因為我不想讓更新過於龐大。)
第三點,另一個問題是關於TTFF*模式效率極低。TTFF*模式週期很短,這應該是一個簡單的分支跳轉預測演算法的應用場景。
然而,問題在於現代處理器不單獨跟蹤每一條分支指令歷史。相反,它們要麼跟蹤所有分支的全域性歷史,要麼它們有一些歷史插槽,每一個都被多分支指令共享。或者,它們可以使用這些技巧與其他技術組合。
所以,if語句中的TTFF模式在到達分支預測器時可能並不是TTFF模式。它可能會和其他分支(在for迴圈體中有兩個分支指令)交錯在一起,可能和其他模式非常接近。但是,我並不是處理器運作方面的專家,如果讀本文的人有不同處理器(尤其是我測試用的Intel Core2)如何運轉的權威參考,請在評論區告訴我。