分支預測(Branch Prediction)
問題:一個排序帶來的差異
public static void main(String[] args) {
int size = 32768;
int data[] = new int[size];
Random random = new Random(0);
for (int i = 0; i < size; i++) {
data[i] = random.nextInt() % 256;
}
// 是否排序
Arrays.sort(data);
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < size; j++) {
if (data[j] >= 128) {
sum += data[j];
}
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
-
未排序耗時:
11.333646239
-
排序後耗時:
3.729554356
-
why ?
Branch Predictor
Ask or Guess direction ?
-
如果猜對了 - 繼續行駛
-
如果猜錯了 - 停車、倒回去、調整軌道,重新啓動、繼續行駛
-
如果每次都猜對 - 永遠不用停車、一直向前
-
如果每次都猜錯 - 反覆停車、倒車、重啓
分支預測(Branch Prediction)
是現代處理器用來提高CPU執行速度的一種手段, 其對程序的分支流程進行預測, 然後預先讀取其中一個分支的指令並解碼來減少等待譯碼器的時間.
維基百科 a strategy in computer architecture design for mitigating the costs usually associated with conditional branches, particularly branches to short sections of code."
How to predict it ?
導致非排序數組耗時顯著增加的 if-statement
if (data[j] >= 128) {
sum += data[j];
}
- T = 分支命中
- N = 分支沒有命中
unsorted data
sorted data
Why need ?
-
CPU Instruction Pipeline
Pipieline假設程序運行時有一連串指令要被運行,將程序運行劃分成幾個階段,按照一定的順序處理
四個執行階段(execuate stage)
:- 讀取指令(
Fetch
) - 指令解碼(
Decode
) - 運行指令(
Execute
) - 寫回運行結果(
Write-back
)
- 讀取指令(
-
分支預測器
分支預測器是一種數字電路,在分支指令執行前,猜測哪一個分支會被執行,條件分支通常有兩路後續執行分支,not token時,跳過接下來的JMP指令,繼續執行, token時,執行JMP指令,跳轉到另一塊程序內存去執行
-
假設沒有分支預測
如果沒有分支預測器,處理器會等待分支指令通過了pipeline的執行階段(execuate stage)才能把下一條指令送入pipeline的fetch stage,這會造成
流水線停頓/冒泡
加入分支預測器後,爲避免流水線停頓,其會猜測兩路分支哪一路最有可能執行,然後投機執行,如果猜錯,則流水線中投機執行中間結果全部拋棄,重新獲取正確分支路線上的指令執行。所以,錯誤的預測也會導致程序執行的延遲。
複雜的流水線,好的分支預測器非常重要
Two Strategy
-
靜態預測
-
編譯時進行
-
任選一條分支:
-
認爲Branch一定會 token
-
認爲Branch一定不會token
-
-
平均命中率: 50%
-
-
動態預測
- 運行時進行
- 根據同一條轉移指令過去的轉移情況來預測未來的轉移情況
- 分支預測緩衝區 -
Branch Prediction Buff
- 分支歷史表 -
Branch history table
- 分支預測緩衝區 -
Some predict model
-
飽和計數
- 強不選擇 - Strongly not taken
- 弱不選擇 - Weakly not taken
- 弱選擇 - Weakly taken
- 強選擇 - Strongly taken
當一個分支命令被求值,對應的狀態機被修改。分支不採納,則向*“強不選擇”方向降低狀態值;如果分支被採納,則向“強選擇”*方向提高狀態值。這種方法的優點是,該條件分支指令必須連續選擇某條分支兩次,才能從強狀態翻轉,從而改變了預測的分支
-
兩級自適應預測器
對於一條分支指令,如果每2次執行發生一次條件跳轉,或者其它的規則發生模式,那麼用上文提到的飽和計 數器就很難預測了。如圖所示,一種二級自適應預測器可以記住過去n次執行該指令時的分支情況的歷史,可 能的2^n種歷史模式的每一種都有1個專用的飽和計數器,用來表示如果剛剛過去的n次執行歷史是此種情況 那麼根據這個飽和計數器應該預測爲跳轉還是不跳轉
How to optimize ?
-
避免在循環中嵌套條件分支. 如果可能,將分支移到外部, 使用多個子循環
do { if (condition_1){ //branch_1 } else if (condition_2){ //branch_2 } else { //branch_3 } //if } while (true);
//改進版本 if (condition_1) { do { //branch_1 } while (true); } else if (condition_2) { do { //branch_2 } while (true); } else { do { //branch_3 } while (true); } //if
-
合併分支條件. 此舉在某種情況下可以大大降低產生錯誤分支預測的概率
if (condition_1 == 0 || condition_2 == 0 || condition_3 == 0) { //branch } //if //改進版本: if ((condition_1 | condition_2 | condition_3) == 0) { //branch } //if
⁉️合併分支條件. 是否真的有必要呢?因爲不優化的時候實際上是有條件短路的可能,避免不必要的計算;而優 化後可能還涉及到cache miss和冗餘計算
-
移除明顯的條件分支, 將執行概率大的條件分支移前
這一條不僅僅有助於規避錯誤分支帶來的性能懲罰, 還減少了不必要的檢測分支條件消耗的CPU時鐘週期.
-
for( ; ; ) and while(true)
// 編譯前 while(true); // 編譯後 mov eax,1 test eax,eax // branch je foo+23h jmp foo+18h
// 編譯前 for(;;); // 編譯後 jmp foo+23h
以上-
謝謝