現代 CPU 深流水線對分支預測失敗極其敏感(誤預測罰 10-20 週期)。在熱點循環中,將 if 判斷轉化為無分支算術表達式,能顯著提升吞吐。本文演示經典「非負求和」案例的三種寫法及性能分析。
// 假設 data[1000][10]
long sum = 0;
for (int i = 0; i < 1000*10; ++i) {
int val = data[i/1000][i%10];
if (val >= 0) // 條件分支
sum += val;
}
問題:數據符號隨機分布時,分支預測器無法有效學習,誤預測率 ~50%。
long sum = 0;
for (int i = 0; i < 1000*10; ++i) {
int val = data[i/1000][i%10];
sum += (val >= 0) * val; // C99: 比較結果 0/1 隱式轉 int
}
原理:(val >= 0) 產生 1 (真) 或 0 (假),乘以 val 等價於條件加法,無控制流跳轉。
; 分支版
.L2:
movsx rdx, DWORD PTR [rsi+rax*4]
test edx, edx
js .L3 ; 條件跳轉
add rdi, rdx
.L3:
add rax, 1
cmp rax, 10000
jne .L2
; 無分支版
.L5:
movsx rdx, DWORD PTR [rsi+rax*4]
test edx, edx
setns sil ; 設置字節: 符號位=0 → 1
movzx esi, sil ; 零擴展到 32 位
imul esi, edx ; 乘法 (或 lea/add 組合)
add rdi, rsi
add rax, 1
cmp rax, 10000
jne .L5
#include <time.h>
#define N 10000000
double bench_branch(int *data) {
clock_t t = clock();
long sum = 0;
for (int i = 0; i < N; ++i) if (data[i] >= 0) sum += data[i];
return (double)(clock() - t) / CLOCKS_PER_SEC;
}
double bench_branchless(int *data) {
clock_t t = clock();
long sum = 0;
for (int i = 0; i < N; ++i) sum += (data[i] >= 0) * data[i];
return (double)(clock() - t) / CLOCKS_PER_SEC;
}
| 版本 | 耗時 | 加速比 |
|---|---|---|
| 分支版 | ~45 ms | 1.00x |
| 無分支版 | ~28 ms | ~1.6x |
數據隨機時分支版誤預測率高,無分支版穩定優勢。若數據多數非負,分支版經預測器優化可能反超。
| 場景 | 推薦 | 理由 |
|---|---|---|
| 熱點循環、數據分布未知/隨機 | ✅ 無分支 | 消除誤預測罰 |
| 循環體極簡、編譯器已自動向量化 | ⚠️ 保持原樣 | 手工干擾可能阻礙 SIMD |
| 條件極其單一 (如 99% 真) | ❌ 保留分支 | 預測器近乎完美 |
| 邏輯複雜、可讀性優先 | ❌ 保留分支 | 過早優化是萬惡之源 |
perf stat -e branch-misses ./prog。-fopt-info-vec 確認編譯器未放棄 SIMD。// branchless: val>=0 ? val : 0 保留可讀性。將判斷融合進表達式是「無分支編程」入門技巧。更高階的 SIMD 內聯、查表法、預取指令,均建立在對數據流與控制流深度理解之上。記住:可讀性是第一生產力,性能優化需數據驅動。