分支預測(Branch Prediction)問題與分析

分支預測(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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lkyEkxuK-1592309980911)(img.assets/image-20200527154106827.png)]

Why need ?

  • CPU Instruction Pipeline

    Pipieline假設程序運行時有一連串指令要被運行,將程序運行劃分成幾個階段,按照一定的順序處理

    四個執行階段(execuate stage):

    • 讀取指令(Fetch)
    • 指令解碼(Decode)
    • 運行指令(Execute)
    • 寫回運行結果(Write-back)
  • 分支預測器

    分支預測器是一種數字電路,在分支指令執行前,猜測哪一個分支會被執行,條件分支通常有兩路後續執行分支,not token時,跳過接下來的JMP指令,繼續執行, token時,執行JMP指令,跳轉到另一塊程序內存去執行

  • 假設沒有分支預測

    如果沒有分支預測器,處理器會等待分支指令通過了pipeline的執行階段(execuate stage)才能把下一條指令送入pipeline的fetch stage,這會造成流水線停頓/冒泡

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tao9W8EC-1592309980913)(img.assets/image-20200527175458021.png)]

    加入分支預測器後,爲避免流水線停頓,其會猜測兩路分支哪一路最有可能執行,然後投機執行,如果猜錯,則流水線中投機執行中間結果全部拋棄,重新獲取正確分支路線上的指令執行。所以,錯誤的預測也會導致程序執行的延遲。

    複雜的流水線,好的分支預測器非常重要

Two Strategy

  • 靜態預測
    • 編譯時進行

    • 任選一條分支:

      1. 認爲Branch一定會 token

      2. 認爲Branch一定不會token

    • 平均命中率: 50%

  • 動態預測
    • 運行時進行
    • 根據同一條轉移指令過去的轉移情況來預測未來的轉移情況
      • 分支預測緩衝區 - Branch Prediction Buff
      • 分支歷史表 - Branch history table

Some predict model

  • 飽和計數

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZjUrbSD5-1592309980915)(img.assets/image-20200527184453108.png)]

    • 強不選擇 - Strongly not taken
    • 弱不選擇 - Weakly not taken
    • 弱選擇 - Weakly taken
    • 強選擇 - Strongly taken

    當一個分支命令被求值,對應的狀態機被修改。分支不採納,則向*“強不選擇方向降低狀態值;如果分支被採納,則向強選擇”*方向提高狀態值。這種方法的優點是,該條件分支指令必須連續選擇某條分支兩次,才能從強狀態翻轉,從而改變了預測的分支

  • 兩級自適應預測器

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zFyZQa2g-1592309980916)(img.assets/image-20200527185835249.png)]

​ 對於一條分支指令,如果每2次執行發生一次條件跳轉,或者其它的規則發生模式,那麼用上文提到的飽和計 數器就很難預測了。如圖所示,一種二級自適應預測器可以記住過去n次執行該指令時的分支情況的歷史,可 能的2^n種歷史模式的每一種都有1個專用的飽和計數器,用來表示如果剛剛過去的n次執行歷史是此種情況 那麼根據這個飽和計數器應該預測爲跳轉還是不跳轉

  • 其他分支預測器
    • 局部分支預測
    • 全局分支預測
    • 融合分支預測
    • 神經分支預測器……

How to optimize ?

  1. 避免在循環中嵌套條件分支. 如果可能,將分支移到外部, 使用多個子循環
    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
    
  2. 合併分支條件. 此舉在某種情況下可以大大降低產生錯誤分支預測的概率
    if (condition_1 == 0 || condition_2 == 0 || condition_3 == 0) {
    		//branch
    } //if
     
    //改進版本:
    if ((condition_1 | condition_2 | condition_3) == 0) {
        //branch
    } //if
    

    ⁉️合併分支條件. 是否真的有必要呢?因爲不優化的時候實際上是有條件短路的可能,避免不必要的計算;而優 化後可能還涉及到cache miss和冗餘計算

  3. 移除明顯的條件分支, 將執行概率大的條件分支移前

    這一條不僅僅有助於規避錯誤分支帶來的性能懲罰, 還減少了不必要的檢測分支條件消耗的CPU時鐘週期.

  4. for( ; ; ) and while(true)
    // 編譯前
    while(true);
    
    // 編譯後
    mov eax,1  
    test eax,eax   // branch
    je foo+23h
    jmp foo+18h
    
    // 編譯前
    for(;;);
    
    // 編譯後
    jmp foo+23h
    

以上-

謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章