如何在程式碼層面提供CPU分支預測效率

張雅宸發表於2022-04-21

關於分支預測的基本概念和詳細演算法可以參考我之前寫的知乎回答,基本概念不再闡述了~~

 

https://www.zhihu.com/question/486239354/answer/2410692045

 

說幾個常見的能夠提升CPU分支預測效率的方法。

將最常見的條件比較單獨從switch中移出

分支預測除了需要預測方向,還需要預測分支的目標地址。目標地址BTA(Branch Target Address)分為兩種:

  • 直接跳轉(PC-relative, direct) : offset以立即數形式固定在指令中,所以目標地址也是固定的。
  • 間接跳轉(absolute, indirect):目標地址來自通用暫存器,而暫存器的值不固定。

對於直接跳轉,使用BTB可以很好的進行預測。但是對於間接跳轉,目標地址不固定,更難預測。switch-case的指令實現(類似jmpq *$rax,$rax是case對應label地址)、C++虛擬函式呼叫就屬於間接跳轉。間接跳轉如果還用直接跳轉的BTB預測,準確率只有50%左右。

很多CPU針對間接跳轉都有單獨的預測器,比如的Intel的論文The Intel Pentium M Processor: Microarchitecture and Performance中介紹額Indirect Branch Predictor:通過額外引入context-information——Global Branch History來提高間接跳轉的目標地址預測準確率。

如何在程式碼層面提供CPU分支預測效率

switch-case的優點是將諸多if/else(conditional branch)轉換為統一的unconditioal branch,但缺點就是目標地址難以預測。如果某個case的命中率特別高,就可以將其從switch中單獨提出來,這樣該分支的預測方向 && 目標地址都很好預測。

比如java dubbo程式碼裡的一個例子:

如何在程式碼層面提供CPU分支預測效率

超過99.9%情況state取值都是ChannelState.RECEIVED ,將其單獨提出來。官網部落格有一個benchmark,效能有很大的改觀。

如何在程式碼層面提供CPU分支預測效率

將使用【控制】的條件轉移轉換為使用【資料】的條件轉移

CMOV指令就是典型的例子。CPU無需進行分支預測,但是會計算一個條件的兩種結果,然後通過檢查條件碼,要麼更新目的暫存器,要麼保持不變。

比如

v = test-expr ? then-expr : else-expr

會轉換為下列虛擬碼:

v = then-expr;
ve = else-expr;
t = test-expr;
if(!t) v=ve;

編譯器會傾向於將使用三元運算子且兩種結果的計算量不大的表示式轉換為CMOV條件資料轉移。例如facebook folly中的例子,注意看註釋:

如何在程式碼層面提供CPU分支預測效率

當分支的結果完全由外部輸入決定,local branch history和global branch history都毫無規律時,效果會更好。下面這個是《Computer Systems A Programmer's Perspective 》5.11.2小節的例子,第二個版本效能是第一個三倍:

/* Rearrange two vectors so that for each i, b[i] >= a[i] */
void minmax1(long a[], long b[], long n) {
  long i;
  for (i = 0; i < n; i++) {
    if (a[i] > b[i]) {
      long t = a[i];
      a[i] = b[i];
      b[i] = t;
    }
  }
}

/* Rearrange two vectors so that for each i, b[i] >= a[i] */
void minmax2(long a[], long b[], long n) {
  long i;
  for (i = 0; i < n; i++) {
    long min = a[i] < b[i] ? a[i] : b[i];
    long max = a[i] < b[i] ? b[i] : a[i];
    a[i] = min;
    b[i] = max;
  }
}

使用算數邏輯代替分支

比如ARM優化手冊裡提到,可以將範圍比較轉換為無條件計算,編譯器有時候也會自動做這個轉換:

// origin version
int insideRange1(int v, int min, int max) {
  return v >= min && v < max;
}

// optimized version
int insideRange2(int v, int min, int max) {
  return (unsigned) (v - min) < (max - min);
}

韋易笑大佬針對這個做過更詳細的優化和測試,反正我是看暈了:

引用文章內的測試資料

如何在程式碼層面提供CPU分支預測效率

Avoiding Branches裡有更多的例子,不過用之前還是做測試更靠譜。

使用template移除分支

2018年Stephen Yang的博士論文NanoLog: A Nanosecond Scale Logging System介紹了一款C++日誌庫Nanolog,將日誌呼叫開銷的中位數降為了個位數納秒級別。作者在文章NANOLOG: A NANOSECOND SCALE LOGGING SYSTEM中提到了Nanolog的關鍵技術和優化,第三條就是將printf在執行時的大量分支邏輯利用C++ template優化成編譯期的運算。

如何在程式碼層面提供CPU分支預測效率

likely/unlikely

這個很多人已經介紹過了,C++20已經將其標準化,支援將更可能執行的程式碼放在hot path上,對icache更友好。例如facebook folly中的例子

如何在程式碼層面提供CPU分支預測效率

FOLLY_LIKELY是一個包裝:

如何在程式碼層面提供CPU分支預測效率

更進一步,有些ISA的分支指令有一個bit,支援programmer去指定分支是否taken。現代CPU使用的TAGE分支預測器,部分實現會使用該bit去初始化predictor(是初始化,不是一直使用programmer指定的跳轉結果)。TAGE預測器可以參考下我開頭放的回答:https://www.zhihu.com/question/486239354/answer/2410692045

 

如何在程式碼層面提供CPU分支預測效率

 

(完)

 

朋友們可以關注下我的公眾號,獲得最及時的更新:

相關文章