無分支程式(branchless code)
既然誤測的分支要付一次管線清空,消除那個風險的一個辦法就是消除分支本身。無分支程式把一個小條件改寫成「用算術與位元技巧算出結果」而不跳躍。中央處理器於是跑一串固定、可預測的指令,路徑上沒有岔路——因此沒有分支要預測,也沒有「若被誤測」的清空。
最簡單的實現用條件移動(x86 的 cmov 指令)或謂詞化(predication):不是「若條件成立就跳並指派 x = a、否則指派 x = b」,而是硬體把兩個候選值都算出來、再依條件選一個,全程不改變指令流。手動時你常用對布林值做算術達成同樣效果:例如不用分支的 max(a, b) 可寫成 b + ((a - b) & -(a > b)),其中 (a > b) 是 0 或 1、-(...) 是全零或全一的遮罩,遮罩決定要不要加上 (a - b)。這類慣用法很多(夾限、取號、絕對值、在兩值間選擇),而編譯器在判定某分支不可預測時,有時會自動產生它們。
要坦白面對取捨,因為無分支不會自動更快。一次條件移動「總是」執行兩條路徑的工作、並對條件製造一個資料相依,所以對一個預測器處理得好的分支而言,它通常比直接分支「更慢」——你每次都付出計算用不到那一側的代價,還可能拉長一條相依鏈。無分支程式恰恰在分支不可預測時取勝(資料相依、接近五五波、沒有可學習模式),此時省下的誤測懲罰勝過浪費掉的工作。誠實的準則是:無分支是針對「已量測到的誤測問題」的定點解藥,而非通用風格。永遠在有代表性的資料上對兩個版本做基準測試。
32 位元整數 x 的無分支絕對值:int m = x >> 31; /* 算術位移:負數時全 1、否則全 0 */ int abs = (x ^ m) - m; 這不用分支就算出 |x|——只有當 x 的正負在周圍迴圈裡不可預測時才有用。
一個經典的無分支慣用法——但只有當它取代的分支本是誤測熱點時才值得。
無分支不是「快」的同義詞:cmov 會執行兩側並在條件上序列化,所以對一個預測良好的分支它反而輸。請量測;別反射性地套用它,並記得編譯器可能已替你發出了 cmov。