Appearance
branch-predict —— 分支预测实验
通过"有序数据+if"、"随机数据+if"、"无分支写法"三种对比,用
perf stat -e branch-misses定量测量分支预测器对规律/随机数据的表现差异。
一、快速开始
bash
make # 编译(生成两个版:默认 -O2 + 无 cmov 版)
make run # 跑全部模式对比表
make perf # `perf stat` 看默认版的分支预测失败率
make perf-no-cmov # `perf stat` 看无 cmov 版的分支预测失败率
make perf-compare # 两版对比二、为什么分支预测重要
现代 CPU 流水线深度 14~20 级,分支预测失败意味着要冲刷流水线,丢失 15~20 个 cycle 的"已发射但错误"的指令。branch-misses / branch-instructions 就是预测失败率:
| 失败率 | 评价 |
|---|---|
| <1% | 优秀——分支很规律 |
| 1~3% | 正常 |
| >5% | 可优化——分支太随机,考虑排序数据或无分支写法 |
前置知识:jcc 是什么
jcc= Jump if Condition is met,x86 条件跳转指令族的统称。不是一条指令,是一组:
指令 全称 C 等价 je/jneJump if Equal / Not Equal if (a == b)/if (a != b)jg/jlJump if Greater / Less if (a > b)/if (a < b)(有符号)jge/jleJump if Greater or Equal / Less or Equal if (a >= b)/if (a <= b)ja/jbJump if Above / Below if (a > b)/if (a < b)(无符号)你写的每一条 if/for/while,最终都编译成
cmp + jcc指令对:asm; C: if (data[i] >= 32768) sum += data[i]; cmp $0x7fff, %eax ; 比较 data[i] 和 32767 jle .skip ; 小于等于 → 跳过累加 add %rax, %rcx ; 累加 .skip:CPU 拿到
jle时,cmp的结果还没算出来(还在流水线里)。它只能猜——猜"跳"还是"不跳"。猜对了流水线继续跑,猜错了冲刷已发射的 15~20 条错误指令,重来。jcc就是分支预测器盯着的物理对象。不是说"这段 C 代码有分支"——精确地说是"这段 C 代码编译后产生了 jcc 指令"。
三、程序结构
三种模式跑同一个判断 if(data[i] >= N/2),区别在数据排列和处理方式:
| 模式 | 数据顺序 | 分支规律 | 预测成功率 |
|---|---|---|---|
| 有序+if | [0,1,2,...,N-1] 排序 | 前半 false、后半 true | >99% |
| 随机+if | 随机打乱 | true/false 随机穿插 | ~50% |
| 无分支 | 随机打乱 | 用算术替代,无分支指令 | 无分支=无误判 |
四、代码设计分析
4.1 数组大小:N=32768 为什么这么选
分支预测实验和缓存实验不同——缓存实验需要数组大到"装不下"才能暴露 miss,分支预测实验恰恰相反:必须让数据访问足够快,否则内存延迟会把分支预测的差异淹没掉。
| 参数 | 值 | 依据 |
|---|---|---|
| N | 32768 | 2^15,对齐友好;int 数组 = 128 KB |
| 数据在缓存的位置 | L2 (1MB) 装得下 | 128 KB × 3 份数组副本 < 1MB,L2 足够 |
| 为什么不能太大 | 如果数据在 L3/DRAM,每次 data[i] 的内存延迟 (40~300 cycles) 远超分支误判 (15~20 cycles),差异被掩盖 |
4.2 阈值 N/2:刻意制造 50/50 最难局面
if(data[i] >= N/2) 让恰好一半元素满足条件。这是对分支预测器最不利的比例:
| true/false 比例 | 预测器策略 | 准确率 |
|---|---|---|
| 90/10 | "始终猜 true" → 90% | 只需知道偏向哪边 |
| 50/50 | 必须依赖历史模式 | 有规律 → 99%;随机 → 50% |
| 99/1 | "始终猜 true" → 99% | 退化为极简策略 |
50/50 是唯一能让"有序 vs 随机"产生最大反差的比例——极端偏斜时,随机数据也能被简单策略("永远猜多数")预测得很好,实验就失去意义了。
4.3 预生成 + 固定种子:消除 rand() 干扰 + 可复现
cpp
std::vector<int> make_random() {
std::vector<int> v(N);
for (int i = 0; i < N; i++) v[i] = i;
std::mt19937 rng(42); // 固定种子,每次跑出相同排列
std::shuffle(v.begin(), v.end(), rng);
return v;
}和缓存实验一样的技巧:数据在计时前就生成好了,rand() 的耗时不计入测试。种子固定为 42 保证同一份代码在任何机器上跑出的 random 排列完全一致,方便跨环境对比。
4.4 volatile long sum:防优化
cpp
volatile long sum = 0;
// ... if(data[i] >= N/2) sum += data[i];
(void)sum; // 显式"使用"一下,双层保险volatile:告诉编译器 sum 可能在"外部"被修改(实际不会),禁止把它优化成寄存器变量或直接消除整个循环。同时也阻断 GCC 对if(cond) volatile_sum += val做 if-conversion(cmov 替换 jmp)——编译器看到 volatile 副作用会保守地保留原始分支。(void)sum:给 lint/编译器的冗余声明,"我知道 sum 被读过"- 没有这两道防线,
-O2会直接把整个循环删掉——因为 sum 的值从未被外部观测
4.5 ROUNDS=1000:为什么循环 1000 遍
每轮 bench_* 调用中,内层循环 32768 次判断 × 外循环 1000 遍 = 3276 万次条件判断。这足够让时钟测量稳定(std::chrono::high_resolution_clock 通常微秒精度),同时总耗时控制在几秒内,不浪费测试时间。
4.6 三种 bench 各自独立:消除数据污染
每个 bench_* 函数内部自己调 make_ordered() / make_random() 生成独立副本:
cpp
double bench_ordered_branch() {
auto data = make_ordered(); // 独立数组
// ... 测量 ...
}而不是共享一个全局数组然后跑三轮。这样每种模式都从"冷缓存"起步,互相不污染,对比公平。
4.7 两版编译:默认 vs 无 cmov
理想中,-O2 看到 if(cond) a = b 这种简单模式会自动生成 cmov(conditional move)取代 jmp 分支;-fno-if-conversion 关闭 cmov,保留原始 jmp 指令。
| 编译目标 | 选项 | 原定意图 |
|---|---|---|
branch_predict | -O2(默认) | 编译器可能用 cmov 自动消除分支 |
branch_predict_no_cmov | -O2 -fno-if-conversion | 强制保留原始 jmp 分支指令 |
bash
make perf-compare # 两版对比但实测发现两版分支指令数几乎完全相同(见"实测数据 → no-cmov 版"):因为 volatile long sum 阻断 if-conversion,且模式 3 的 int mask = (cond) ? -1 : 0 本身就是三元式分支——关掉一个不存在的优化当然没变化。这个"打脸"本身就是教学点:volatile 不止防死循环消除,也防分支消除。
编译器什么时候能自动消除分支
| 能优化 | 不能优化 |
|---|---|
x = cond ? a : b; 简单赋值 | 分支内有函数调用 |
if(cond) x = a; else x = b; 纯写寄存器 | volatile 变量参与 |
| 结果类型是标量(int/指针/float) | 分支内有内存 store |
| 两个分支都很短(1~3 条指令) | 分支体超过 ~4 条指令(cmov 延迟反而更大) |
| 数据随机(无规律) | 分支预测已经很准(>99%)——编译器会保留 jcc,因为 jcc 猜对比 cmov 快 |
编译器的优化逻辑是成本收益分析:cmov 消除了分支误判风险,但引入了数据依赖链(cmov 的 dest 依赖两个 src 都就绪)。当分支预测准确率高时,jcc 的"零延迟"反而更优——这就是为什么 -O2 下编译器看到规律分支会主动保留 jcc,看到随机分支才转 cmov。但这一决策依赖 profile 数据(-fprofile-use),默认 -O2 只能静态猜测。
相关编译选项:
bash
-fif-conversion # 开启 if-conversion(-O2/-O3 默认)
-fno-if-conversion # 关闭(本实验 no-cmov 版所用)
-fprofile-generate / -fprofile-use # PGO:给编译器真实数据分布,决策更准4.8 什么 C/C++ 代码会产生分支指令
cpp
// ── 以下每种写法都会生成 jcc(条件跳转)── //
// 1. if/else —— 最多见
if (data[i] >= threshold)
sum += data[i];
// 2. 三元运算符 — 简单场景编译器可能转 cmov,复杂场景仍生成 jcc
int result = (a > b) ? func1(x) : func2(x); // 有函数调用 → jcc
int mask = (cond) ? -1 : 0; // 简单常量 → cmov/jcc 取决于编译器
// 3. && 和 || —— 短路求值,每多一个条件就多一条 jcc
if (ptr != nullptr && ptr->value > 0 && !ptr->done) { ... }
// ↑─────────────↑ jcc ─────↑ jcc ───────↑ jcc
// 4. for/while 循环条件 —— 每次迭代一条 jcc
for (int i = 0; i < N; i++) { ... } // i < N → jcc
// 5. switch —— 跳转表(indirect jmp) 或 cmp+jcc 链
switch (state) { case 0: ... case 1: ... } // 分支多→跳转表,少→if-else链
// 6. 虚函数 / 函数指针 —— 间接跳转(分支目标预测,与方向预测不同)
obj->virtual_method(); // vtable → indirect call → BTB 预测目标地址
void (*fp)() = handler; // fp() → indirect call不会产生分支(或被编译器消除)的写法:
cpp
// cmov —— 编译器在 -O2 下自动转换(需满足"能优化"表格的条件)
x = cond ? a : b; // → cmov(无 jcc)
// 位运算替代 —— 用户手写无分支
int sign = (x >> 31) | 1; // 取符号,无分支
int abs = (x ^ (x>>31)) - (x>>31); // 绝对值,无分支4.9 如何写无分支代码
技巧 1:算术掩码(本实验模式 3 的思路)
cpp
// 有分支:
if (data[i] >= N/2)
sum += data[i];
// 无分支: 用 mask 做"条件开关"
int mask = (data[i] >= N/2) ? -1 : 0; // true→0xFFFFFFFF, false→0x00000000
sum += data[i] & mask; // mask=全1→保留, mask=0→清零
// 最终编译形态(-O2 下):
// cmp ...; setge al; neg eax; and ...; add ...
// ↑ setge 按条件置位,neg 转成全1/全0,没有 jcc 指令技巧 2:查表法(O(1),无分支)
cpp
// 有分支:
if (x < 0) result = 0;
else if (x < 10) result = 1;
else result = 2;
// 无分支: 把条件映射到数组下标
const int table[] = {0, 1, 2}; // 下标 = 分类号
int idx = (x >= 0) + (x >= 10); // 0→0, 1→1, 2→2
result = table[idx]; // 无 jcc,只有算术 + 访存技巧 3:位运算替代常见模式
cpp
// 取绝对值 —— 不用 if(x<0) 分支
int abs_val = (x ^ (x >> 31)) - (x >> 31);
// 取两数最小值 —— 不用 if(a<b) 分支
int min_val = b + ((a - b) & ((a - b) >> 31));
// 取两数最大值
int max_val = a - ((a - b) & ((a - b) >> 31));
// 判断符号(正→1,负→-1)
int sign = (x > 0) - (x < 0); // 或: (x >> 31) | 1技巧 4:让编译器替你无分支(最省心的方式)
cpp
// 只要满足 4.7 的"能优化"条件,直接写三元式,让编译器生成 cmov:
double result = (data[i] >= threshold) ? data[i] : 0.0;
// 注意:GCC/Clang -O2 会自动判断 cmov vs jcc 哪个更快
// 如果有 PGO 数据 (-fprofile-use),决策会更准什么时候不要用无分支
| 场景 | 原因 |
|---|---|
| 分支体内有复杂计算(>3~4 条指令) | cmov 等待两个 src 都就绪,延迟比 jcc+预测 大 |
| 数据有强规律(>99% 预测命中) | if 几乎零开销,无分支多出来的算术指令反而慢 |
| 浮点运算 | cmov 在某些 CPU 上不支持浮点操作数 |
| 代码可读性第一 | if 比 (x>>31) & m 好理解一百倍——除非 profile 证明这里有瓶颈 |
总结这个选择:无分支像"买保险"——防止流水线冲刷,但每次都要付算术指令的成本。分支预测像"赌方向"——赌对了零成本,赌错了付 15~20 cycle。你的工作是在知道数据分布之后,选总成本更低的那条路。
五、预期发现(理论预测)
| 有序+if | 随机+if | 随机+无分支 | |
|---|---|---|---|
| branch-miss 率 | <1% | ~50% | ~0% |
| 吞吐 | 最快 | 最慢(~50% of 有序) | 居间(比随机 if 快,比有序 if 慢) |
六、实测数据
6.1 吞吐对比(make run)
bash
模式 吞吐(亿次/s) 相对(%)
有序数据 + if 分支 0.601 100.0%
随机数据 + if 分支 0.295 49.1%
随机数据 + 无分支(算术) 0.796 132.5%结果和"预期发现"不同:无分支不是"居间",而是比有序+if 还快 32.5%。三个反直觉点:
反直觉点 1:随机+if = 49.1%,不是 50% —— 比"两次判断平均对一次"略好。
命中那次 = cmp + jcc ≈ 2 cycle;未命中 = 同上 + 流水线冲刷 ≈ 15~20 cycle。平均 ~10 cycle,理论应掉到 20%。实测只掉到 49%,说明现代 CPU 的冲刷惩罚比教科书小(乱序执行 + 微 op cache 兜底)。
反直觉点 2(核心):随机+无分支比有序+if 还快 32.5%。
| 有序 + if | 随机 + 无分支 | |
|---|---|---|
| 指令/次判断 | cmp + jcc | cmp + cmov(或 setg + and) |
| 分支指令开销 | jcc 占 BTB 一条、占分支单元端口 | 零分支指令 |
| 前端 fetch | 两条路径都要缓存 | 单路径 |
| 后端并行度 | jcc 切两段 | 可和后续指令重叠 |
分支预测不是"免费的"——即使 100% 命中,jcc 本身就要消耗前端端口、BTB 槽位、分支单元。数据在 L2 的前提下,消除分支带来的指令级并行收益 > 分支预测的收益。
注意:本实验里
volatile long sum阻断了编译器做 if-conversion,所以模式 1/2 的 if 是原始 jmp 分支,模式 3 的无分支是用户代码层面的算术替代——132.5% 的反超是 jmp 开销 vs cmov 开销的对比。
反直觉点 3:有序+if 不是天花板。
bash
吞吐排名(实测):
1. 随机 + 无分支(算术) 132.5%
2. 有序 + if 分支 100.0%
3. 随机 + if 分支 49.1%6.2 perf stat:默认 -O2 版
bash
$ make perf
perf stat -e cycles,instructions,branch-instructions,branch-misses \
./branch_predictbash
778,216,278 cycles:u # 1.19 insn per cycle
924,210,991 instructions:u # 0.207 CPU utilized
164,754,918 branch-instructions:u
15,981,117 branch-misses:u # 9.70% of all branches
0.2260584757 seconds time elapsed| 指标 | 值 | 说明 |
|---|---|---|
| IPC | 1.19 | 远低于理论 4~6,还有提升空间(分支消除) |
branch-instructions / instructions | 17.8% | 包含模式 1/2 的 jmp + 模式 3 的 mask 三元式 + for 循环退出分支 |
branch-misses 率 | 9.70% | 三种模式 各 1/3 权重混合:模式 2 的 50% miss 拖高,模式 1/3 接近 0 miss |
6.3 perf stat:no-cmov 版(关键:数字几乎没变)
bash
$ make perf-no-cmov
perf stat -e cycles,instructions,branch-instructions,branch-misses \
./branch_predict_no_cmovbash
777,348,221 cycles:u # 1.19 insn per cycle
924,210,934 instructions:u
164,754,939 branch-instructions:u
15,980,022 branch-misses:u # 9.70% of all branches
0.235204364 seconds time elapsed对比:
| 指标 | 默认 -O2 | no-cmov 版 | 差 |
|---|---|---|---|
| cycles | 778,216,278 | 777,348,221 | -0.1% |
| instructions | 924,210,991 | 924,210,934 | ≈0 |
| branch-instructions | 164,754,918 | 164,754,939 | +21(噪声) |
| branch-misses | 15,981,117 | 15,980,022 | -0.007% |
| IPC | 1.19 | 1.19 | 一样 |
为什么 -fno-if-conversion 没效果?
-O2默认本来就没做 if-conversion:volatile long sum阻止编译器用 cmov 替换 jmp——volatile 副作用让编译器保守地保留原始分支。- 模式 3 代码本身含三元式分支:
int mask = (cond) ? -1 : 0——main.cpp 注释写"仍含分支,展示概念用"。这条三元式在两个版本里都被编译为分支指令。 - 所以 no-cmov 版"关掉的优化本来就不存在"——两版看到的都是原始分支。
这个"失效"的教学价值:volatile 不止防死循环消除,也防分支消除(if-conversion)。在防优化与让编译器优化之间,是一对矛盾。这也是本实验选择 volatile + -O2 组合的内在原因。
函数级 miss 分布(perf report --stdio,no-cmov 版实测):
bash
perf record -e branch-misses:ppu -g -F 4000 ./branch_predict_no_cmov
perf report --stdiobash
# Samples: 575 of event 'branch-misses:ppu'
# Event count (approx.): 16030374
#
# Children Self Command Shared Object Symbol
#
99.80% 99.80% branch_predict_ branch_predict_no_cmov [.] bench_random_branch
|
---bench_random_branch
0.07% 0.07% branch_predict_ branch_predict_no_cmov [.] std::mersenne_twister_engine<unsigned long, 32ul, 624ul, ...>
0.04% 0.04% branch_predict_ ld-2.17.so [.] strncmp
0.02% 0.02% branch_predict_ ld-2.17.so [.] do_lookup_x
0.02% 0.02% branch_predict_ branch_predict_no_cmov [.] bench_ordered_branch
0.02% 0.02% branch_predict_ ld-2.17.so [.] check_match.9525
...
0.01% 0.01% branch_predict_ libstdc++.so.6.0.28 [.] std::__cxx11::moneypunct<...>::_M_initialize_moneypunct
...| 函数 | Self % | 解读 |
|---|---|---|
bench_random_branch | 99.80% | 575 个 samples 中独占 574 个——模式 2 是 branch-miss 的绝对主导 |
bench_ordered_branch | 0.02% | 模式 1(有序+if),分支预测几乎 100% 命中 |
std::mersenne_twister... | 0.07% | RNG 内部分支——噪声 |
strncmp / do_lookup_x / 其他 | <0.04% | libc / ld / 启动代码——噪声 |
branch-misses 9.70% 的来源破解:
bash
加权 miss 率 = 模式1_miss × 1/3 + 模式2_miss × 1/3 + 模式3_miss × 1/3
9.70% = ~0% × 1/3 + 模式2_miss × 1/3 + ~0% × 1/3解得 模式 2 的纯分支失败率 ≈ 9.70% × 3 ≈ 29%——远低于理论 50%。
与反直觉点 1 互相印证:
| 维度 | 模式 2 实际 | 模式 2 理论 |
|---|---|---|
| 吞吐(相对有序) | 49.1% | 50% |
| 分支失败率 | ~29% | 50% |
两者都偏离理论 50% 一点点、偏向"没那么糟"——说明现代 CPU 的分支冲刷惩罚没教科书上那么大:乱序执行会提前执行不依赖分支结果的指令、微 op cache 兜底一部分开销、PEBS 采样也只是近似(不一定 100% 覆盖所有 miss 事件)。
七、perf 命令速查
7.1 基础统计
bash
perf stat -e cycles,instructions,branch-instructions,branch-misses \
./branch_predict # 默认版
perf stat -e cycles,instructions,branch-instructions,branch-misses \
./branch_predict_no_cmov # 无 cmov 版(本实验两版数一样)7.2 汇编级定位
bash
perf record -e branch-misses:ppu -g -F 4000 ./branch_predict_no_cmov
perf report --stdio # 函数级 miss 分布
perf annotate # 指令级 miss 分布(看具体哪条 jcc 在 miss):ppu (precise, userspace) 后缀需要 PEBS 支持(Intel Core 2+ 或 AMD Zen+)。-F 4000 限定采样频率,避免高开销采样淹没被测函数。
perf annotate 实测输出(定位到 bench_random_branch 内部):
bash
Samples: 575 of event 'branch-misses:ppu' (4000 Hz, Event count: 16030374)
Percent
...
: ZI19bench_random_branchv():
: for (int r = 0; r < ROUNDS; r++) {
: for (int i = 0; i < N; i++) {
: if (data[i] >= N / 2) // true/false 完全随机
0.22 ╭ 38: movslq (%rdx),%rax
99.78 │ cmp $0x3fff,%eax
╰ ←→ jle 4f ← 这条 jcc 就是 99.78% miss 的源头
4f: add %rax,%rcx
...
}| 元素 | 含义 |
|---|---|
movslq (%rdx), %rax | 0.22% — 加载 data[i],偶尔因 D-cache miss 被采样到 |
cmp $0x3fff, %eax | 数据准备,几乎不 miss |
jle 4f (line 38) | 99.78% 本地采样 —— if(data[i] >= N / 2) 的 jcc 指令,true/false 完全随机 |
add %rax, %rcx | 0% — 累加到 sum,不分支 |
结论定位:99.78% miss 集中在单条 jcc 指令——直接证明"模式 2 的随机数据 → if 判断 → jcc 几乎 50% 失败 → 流水线冲刷 → 吞吐降到 49.1%"这条因果链。配合函数级 perf report 看 99.80% 都来自 bench_random_branch——双重佐证模式 2 是绝对瓶颈。
7.3 间接分支
面向对象代码中多态调用频繁时关注:
bash
perf stat -e branch-loads,branch-load-misses ./branch_predict八、实验结论
结论一:随机数据对分支预测是灾难
if(data[i] >= N/2) 在数据排好序时能达到 >99% 预测准确率,吞吐 0.601 亿次/s,IPC 1.19。
数据随机打乱后,吞吐掉到 0.295 亿次/s(减半到 49.1%),IPC 跌到 0.6 左右。
perf 证据:
perf report显示 99.80% 的 branch-misses 集中在bench_random_branch一个函数,perf annotate定位到单条cmp $0x3fff, %eax; jle 4f贡献了 99.78% 的本地 miss。一条 C 语言的 if 语句 → 一条 x86 的 jcc 指令 → 所有性能灾难的根源。
教训:数据分布决定分支预测成败,不是代码写得好不好的问题。
结论二(最反直觉):消除分支比完美预测更好
这是实验最大的发现——无分支写法(0.796 亿次/s)比有序+if(0.601 亿次/s)快了 32.5%。
bash
吞吐排名(实测):
1. 随机 + 无分支(算术) 132.5%
2. 有序 + if 分支 100.0%
3. 随机 + if 分支 49.1%为什么"完美预测的分支"还不如"没有分支"?
| 有序 + if(100% 预测命中) | 随机 + 无分支 | |
|---|---|---|
| 核心指令 | cmp + jcc | cmp + cmov(或 setg + and) |
| jcc 开销 | BTB 占一条、占分支单元端口、占前端 fetch | 零 jcc 指令 |
| 指令级并行度 | jcc 把数据流切两段 | 无分支可连续发射 |
分支预测不是免费的——即使 100% 命中,jcc 这个"有"分支本身就是一种开销:BTB 槽位、前端 fetch 压力、流水线切段。在数据不卡缓存(L2 命中)时,这些开销的总和 > 分支预测带来的节省。
这也解释了为什么编译器在
-O2下看到if(cond) a = b这种简单赋值时会主动用cmov替换 jcc——编译器不关心"预测器能不能猜对",它知道直接消灭分支比猜对更快。本实验因为volatile阻止了自动 if-conversion,所以无分支的收益来自用户代码层面。
教训:不要迷信"预测率 100% = 最优"。如果代码的热路径上 jcc 指令本身成了瓶颈(分支密集、IPC 偏低),即使预测器全中也可以考虑无分支。
结论三:什么时候该用哪种
| 场景 | 推荐 | 原因 |
|---|---|---|
| 数据有规律(排好序、90/10 偏斜) | if 分支 | 预测 >99%,代码清晰,无额外计算 |
| 数据完全随机 | 无分支 | 比 if 快 1.7x(49% vs 132%) |
| 数据可排序 | 排序 → if | 一次排序的 O(N log N) 换 O(N) 无分支的稳定预测 |
| 浮点、复杂逻辑、有副作用 | if 分支 | cmov 不支持所有类型,复杂无分支可读性差 |
| 不确认数据分布 | profile 再说 | 别猜——perf stat -e branch-misses 看一眼 |
结论四:日常编码建议
原则:先 profile,再优化;先可读,再无分支。
bash
你的优先级:
1. 写清晰的 if/else ← 默认首选,可读性第一
2. 跑 perf stat -e branch-misses ← 看热点分支 miss 率
3. miss > 5% → 考虑优化 ← 只有这时候才动手
4. 数据可排序 → 排序后用 if ← 一次 O(N log N) 换 O(N) 稳定预测
5. 数据不可排序 → 无分支 ← 算术/位运算/查表写代码时养成三个习惯:
| 习惯 | 做法 | 为什么 |
|---|---|---|
循环里避免 if 调函数 | 把调用提到循环外 | 函数调用阻止 cmov,且函数调用本身也有分支 |
| 知道数据分布时直接告诉排序器 | std::sort 预排序,而不是硬扛随机 | 一次排序代价 << 千万次 jcc 误判 |
| 密集 if 链考虑查表/switch | 4~5 个连续的 if(x==c) → switch 或查表 | switch → 跳转表(O(1)),if 链 → O(n) |
什么情况必须用 if(不要无分支):
- 分支内有副作用(
new/delete/ I/O / 文件操作)——不能同时执行两条路径 - 分支体大(>4 条指令)—— cmov 的数据依赖延迟 > jcc+预测 的成本
- 分支在冷路径上(99% 情况不执行)——优化热点才有效
- 团队代码规范要求可读性优先——
if比(x>>31)^...好维护
一句话排查法:
bash
perf stat -e branch-misses ./your_program
# miss > 5% → perf record -e branch-misses:ppu -g
# → perf report --stdio → 看哪个函数 → perf annotate → 看哪条 jcc
# → 判断数据有无规律 → 排序 or 无分支一句话
分支预测器是"模式识别器"——规律的分支几乎零开销,随机的分支每次误判浪费 15~20 cycles。但本实验最重要的发现是:无分支甚至比完美预测的 if 还快 32.5%——消除 jcc 指令本身的开销 > 分支预测完美命中。所以最优策略不是"让预测器猜得更准",而是"让分支不要存在"。工程上一句话:先 profile 再优化,先可读再无分支。
九、关联文档
- tools/code/perf.md——perf 工具主文档,§六.1.3 分支预测事件
- concepts/tools/perf-demos-architecture.md——perf 12 场景学习架构总纲
- concepts/cpu/pmu.md——PMU 硬件架构与分支预测器原理