mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2442 字
7 分钟
分支预测:如果猜错了代价多大
2026-03-05

你写了一个 if-else,CPU 需要在知道条件结果之前就决定取哪条路径的指令。猜对了,流水线继续满载运行;猜错了,冲刷流水线、重新取指——代价是 15-20 个时钟周期。现代 CPU 的分支预测准确率超过 97%,但那 3% 的失败可能吃掉 30% 的性能。

现在来看解析分支预测的硬件机制——量化预测失败的代价,并探讨软件层面如何减少分支预测失败。

一、为什么需要分支预测?#

1.1 分支无处不在#

程序中大约每 5-7 条指令就有一条分支指令(if/else、for、while、switch、函数调用)。如果 CPU 每次遇到分支都停顿等待结果,流水线将频繁空转。

以 19 级流水线为例:

场景分支比例预测策略平均 CPI
无分支0%不需要~0.5
有分支,完美预测15%100% 准确~0.55
有分支,不预测(停顿)15%等待结果~3.5
有分支,随机预测15%50% 准确~2.0

不预测的代价是7 倍性能下降。分支预测不是”锦上添花”,而是”生死攸关”。

1.2 预测失败的代价#

flowchart LR subgraph 预测成功["预测成功"] S1["取指预测路径"] --> S2["解码"] --> S3["执行"] --> S4["确认正确"] --> S5["继续"] end subgraph 预测失败["预测失败"] F1["取指预测路径"] --> F2["解码"] --> F3["执行"] --> F4["发现错误!"] F4 --> F5["冲刷流水线<br/>15-20 周期"] F5 --> F6["重新取指正确路径"] F6 --> F7["重新填充流水线"] end style 预测成功 fill:#e8f5e9,stroke:#2e7d32 style 预测失败 fill:#ffcdd2,stroke:#c62828 style F5 fill:#ff5252,color:#fff

预测失败的代价 = 流水线深度 + 重新取指延迟。对于现代 CPU:

CPU流水线深度预测失败惩罚(周期)@3GHz 延迟
Intel Skylake14-1915-20~6 ns
AMD Zen 414-1916-22~6 ns
Apple M11412-15~4 ns

二、静态预测#

2.1 最简单的策略#

静态预测不依赖运行时信息,仅根据指令特征做出预测:

策略规则准确率
总是预测不跳转BTFN(Backward Taken, Forward Not-taken)~60-70%
向后跳转预测跳转循环通常执行多次~70-80%
基于编译器提示likely/unlikely 属性~75-85%

2.2 编译器提示#

Linux 内核大量使用 likely/unlikely 宏:

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
// 示例:错误路径标记为 unlikely
if (unlikely(ptr == NULL)) {
return -EINVAL; // 编译器将此路径放在远处
}
// 示例:正常路径标记为 likely
if (likely(cpu_online(cpu))) {
// 编译器将此路径紧随 if 之后
schedule_on_cpu(cpu);
}

编译器通过调整分支的布局来帮助 CPU 的静态预测:

  • likely 路径放在 if 后面(顺序执行,不跳转)
  • unlikely 路径放在远处(需要跳转才能到达)

三、动态预测#

3.1 1-bit 预测器#

最简单的动态预测器:用 1 个 bit 记录上次分支的结果。

上次结果预测问题
跳转预测跳转循环最后一次迭代预测错误
不跳转预测不跳转循环第一次迭代预测错误

1-bit 预测器对循环嵌套的准确率较低——一个循环执行 100 次跳转 + 1 次不跳转,1-bit 预测器会错误 2 次(第一次和最后一次),准确率 99/101 ≈ 98%。

3.2 2-bit 饱和计数器#

现代 CPU 使用 2-bit 饱和计数器(Bimodal 预测器),需要连续两次预测错误才会改变预测方向:

stateDiagram-v2 [*] --> StrongNT : 初始 StrongNT --> WeakNT : 跳转(错误) WeakNT --> StrongNT : 不跳转(正确) WeakNT --> WeakT : 跳转(错误) WeakT --> StrongT : 跳转(正确) StrongT --> WeakT : 不跳转(错误) WeakT --> WeakNT : 不跳转(错误) WeakNT --> StrongNT : 不跳转(正确) StrongNT --> StrongNT : 不跳转(正确) StrongT --> StrongT : 跳转(正确) note right of StrongNT : 预测:不跳转 note right of WeakNT : 预测:不跳转 note right of WeakT : 预测:跳转 note right of StrongT : 预测:跳转

2-bit 计数器的状态转换:

状态预测跳转后不跳转后
Strong Not-Taken00不跳转01 (Weak NT)00 (Strong NT)
Weak Not-Taken01不跳转10 (Weak T)00 (Strong NT)
Weak Taken10跳转11 (Strong T)01 (Weak NT)
Strong Taken11跳转11 (Strong T)10 (Weak T)

2-bit 预测器对循环的准确率:100 次跳转 + 1 次不跳转 → 只有 1 次预测错误,准确率 100/101 ≈ 99%。

3.3 局部预测器与全局预测器#

2-bit 预测器无法处理相关性——分支的结果可能依赖于之前分支的历史。

示例:嵌套 if 语句

if (a > 0) { ... } // 分支 B1
if (b > 0) { ... } // 分支 B2
if (a > 0 && b > 0) { ... } // 分支 B3
// B3 的结果完全由 B1 和 B2 决定!

全局预测器(GShare):使用**全局分支历史寄存器(GHR)**记录最近 N 个分支的结果,与 PC 做异或后索引预测表。

graph LR PC["PC 地址"] --> XOR["XOR"] GHR["全局历史寄存器<br/>GHR<br/>[T,NT,T,T,NT,...]"] --> XOR XOR --> TABLE["预测表<br/>PHT<br/>2-bit 计数器数组"] TABLE --> PRED["预测结果"] ACTUAL["实际结果"] --> UPDATE["更新 GHR 和 PHT"] PRED --> UPDATE style GHR fill:#fff9c4,stroke:#f9a825 style TABLE fill:#e3f2fd,stroke:#1565c0

局部预测器:为每个分支地址维护独立的历史记录。

预测器类型索引方式优势劣势
BimodalPC 直接索引简单无相关性
GSharePC ⊕ GHR捕获全局相关性别名冲突
局部PC → 历史表 → 预测表捕获局部相关性面积大
混合(Tournament)选择最优预测器最高准确率最复杂

3.4 混合预测器(Tournament Predictor)#

现代高性能 CPU 使用混合预测器,同时运行多个预测器,通过选择器决定使用哪个预测结果:

graph TB BRANCH["分支指令<br/>PC"] --> GLOBAL["全局预测器<br/>GShare"] BRANCH --> LOCAL["局部预测器<br/>2-level"] BRANCH --> SELECTOR["选择器<br/>2-bit 计数器"] GLOBAL --> CHOICE{"选择器决定"} LOCAL --> CHOICE SELECTOR --> CHOICE CHOICE --> |"全局更优"| USE_GLOBAL["使用全局预测"] CHOICE --> |"局部更优"| USE_LOCAL["使用局部预测"] style SELECTOR fill:#fff9c4,stroke:#f9a825 style GLOBAL fill:#e3f2fd,stroke:#1565c0 style LOCAL fill:#e8f5e9,stroke:#2e7d32

Intel 的分支预测器准确率:

CPU预测准确率预测器类型
Pentium 4~94%混合
Core 2~97%混合
Skylake~97.5%混合 + TAGE
Golden Cove~98%+TAGE

四、BTB:分支目标缓存#

4.1 为什么需要 BTB?#

即使预测了分支方向,CPU 还需要知道跳转的目标地址。对于条件分支,目标地址在解码后才知道;对于间接跳转(函数指针、虚函数),目标地址在执行后才知道。

BTB(Branch Target Buffer) 缓存分支的目标地址,使得 CPU 在取指阶段就能知道跳转目标。

4.2 BTB 的结构#

┌──────────┬──────────────┬──────────┐
│ 分支 PC │ 目标地址 │ 预测信息 │
├──────────┼──────────────┼──────────┤
│ 0x401000 │ 0x401050 │ Taken │
│ 0x401020 │ 0x401100 │ Not Taken│
│ 0x401050 │ 0x401200 │ Taken │
│ ... │ ... │ ... │
└──────────┴──────────────┴──────────┘

4.3 多级 BTB#

现代 CPU 使用多级 BTB,类似于缓存层次:

级别容量延迟用途
L1 BTB~64-128 条目1 周期最近使用的分支
L2 BTB~4K-8K 条目2-3 周期更多分支
L3 BTB(部分 CPU)~16K+ 条目3-5 周期全局分支历史

五、RAS:返回地址栈#

5.1 函数返回的特殊性#

函数调用(call)和返回(ret)是最频繁的分支类型之一。与普通分支不同,函数返回的目标地址是动态的——同一个 ret 指令可能返回到不同的调用点。

int foo() { return bar(); } // bar() 返回到 foo
int baz() { return bar(); } // bar() 返回到 baz
// bar() 中的 ret 指令需要返回到不同地址!

5.2 RAS 的工作原理#

RAS(Return Address Stack)是一个硬件栈,call 时压入返回地址,ret 时弹出:

sequenceDiagram participant Main participant RAS participant Foo participant Bar Main->>RAS: call foo → 压入 0x1000 Note over RAS: [0x1000] Foo->>RAS: call bar → 压入 0x2000 Note over RAS: [0x1000, 0x2000] Bar->>RAS: ret → 弹出 0x2000 Note over RAS: [0x1000] Bar-->>Foo: 返回到 0x2000 Foo->>RAS: ret → 弹出 0x1000 Note over RAS: [] Foo-->>Main: 返回到 0x1000

RAS 的容量有限(通常 16-64 个条目),深度递归会溢出。溢出后,最旧的条目被覆盖,导致返回地址预测失败。

5.3 间接跳转预测#

虚函数调用、函数指针等间接跳转无法用 RAS 预测。现代 CPU 使用间接分支预测器(Indirect Branch Predictor),类似于 BTB 但使用更复杂的索引方式(包含历史信息)。

六、预测失败的代价量化#

6.1 经典实验:排序 vs 未排序#

这是分支预测最经典的实验:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 32768
int main() {
int *data = malloc(SIZE * sizeof(int));
for (int i = 0; i < SIZE; i++) data[i] = rand() % 256;
// 测试 1:未排序数据
clock_t start = clock();
long long sum = 0;
for (int j = 0; j < 10000; j++) {
for (int i = 0; i < SIZE; i++) {
if (data[i] >= 128) sum += data[i];
}
}
clock_t end = clock();
printf("未排序: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
// 排序
qsort(data, SIZE, sizeof(int), (int(*)(const void*,const void*))strcmp);
// 测试 2:排序数据
start = clock();
sum = 0;
for (int j = 0; j < 10000; j++) {
for (int i = 0; i < SIZE; i++) {
if (data[i] >= 128) sum += data[i];
}
}
end = clock();
printf("已排序: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
free(data);
return 0;
}

典型结果:已排序版本快 2-3 倍。原因:

  • 未排序数据:data[i] >= 128 的结果随机,分支预测准确率约 50%
  • 排序数据:前半段全是 < 128,后半段全是 >= 128,分支预测准确率接近 100%

6.2 用 perf 量化分支预测失败#

perf stat -e branches,branch-misses ./branch_test
# 未排序版本:
# 327,680,000 branches
# 163,840,000 branch-misses # 50.00% of all branches
# 排序版本:
# 327,680,000 branches
# 256,000 branch-misses # 0.08% of all branches

6.3 分支预测失败对整体性能的影响#

预测准确率分支比例有效 CPI 增加性能损失
100%15%0%0%
99%15%~3%~3%
97%15%~9%~8%
95%15%~14%~12%
90%15%~29%~22%
50%15%~143%~59%
Warning

分支预测准确率从 97% 降到 95% 看似微小(2%),但性能损失从 8% 跳到 12%。这是因为每次预测失败的惩罚是固定的(15-20 周期),准确率越低,失败越频繁,损失越大。

七、软件层面的分支优化#

7.1 用条件传送替代分支#

// 分支版本
int max_branch(int a, int b) {
if (a > b) return a;
else return b;
}
// 条件传送版本(CMOV)
int max_cmov(int a, int b) {
return (a > b) ? a : b; // 编译器可能生成 CMOV
}

x86 的 CMOV(Conditional Move)指令:不改变控制流,根据条件选择源操作数。代价是两条路径都要计算,但避免了分支预测失败。

7.2 用位运算消除分支#

// 分支版本:计算绝对值
int abs_branch(int x) {
if (x < 0) return -x;
return x;
}
// 无分支版本
int abs_branchless(int x) {
int mask = x >> 31; // 0x00000000 或 0xFFFFFFFF
return (x ^ mask) - mask;
}
// 分支版本:clamp
int clamp_branch(int x, int lo, int hi) {
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
// 无分支版本
int clamp_branchless(int x, int lo, int hi) {
// 利用无符号比较的溢出特性
return x < lo ? lo : (x > hi ? hi : x);
// 更优的位运算版本省略
}

7.3 循环展开#

// 普通循环:每次迭代都有循环分支
for (int i = 0; i < n; i++) {
sum += arr[i];
}
// 展开后:循环分支减少 4 倍
for (int i = 0; i < n; i += 4) {
sum += arr[i];
sum += arr[i+1];
sum += arr[i+2];
sum += arr[i+3];
}
// 处理剩余元素...

7.4 数据预处理#

// 原始版本:在热循环中有分支
for (int i = 0; i < n; i++) {
if (data[i].type == TYPE_A) {
process_a(&data[i]);
} else {
process_b(&data[i]);
}
}
// 优化:先按类型分组,再分别处理
// 分组后每组内部无分支
int count_a = 0, count_b = 0;
for (int i = 0; i < n; i++) {
if (data[i].type == TYPE_A) group_a[count_a++] = data[i];
else group_b[count_b++] = data[i];
}
for (int i = 0; i < count_a; i++) process_a(&group_a[i]);
for (int i = 0; i < count_b; i++) process_b(&group_b[i]);

八、分支预测与安全:Spectre 攻击#

8.1 Spectre 的原理#

2018 年的 Spectre 攻击利用了分支预测的副作用:

  1. 训练分支预测器,使其预测跳转到恶意代码路径
  2. CPU 推测执行恶意路径,访问敏感数据 3.虽然推测结果最终被丢弃,但缓存副作用保留了
  3. 通过缓存侧信道(Flush+Reload)推断敏感数据
sequenceDiagram participant 攻击者 participant CPU participant 缓存 participant 受害者数据 攻击者->>CPU: 训练分支预测器(多次执行使预测器"学习") Note over CPU: 分支预测器被"毒化" 攻击者->>CPU: 触发受害者代码的分支 CPU->>CPU: 预测跳转到恶意路径 CPU->>受害者数据: 推测执行:读取敏感数据 受害者数据->>缓存: 敏感数据被加载到缓存 CPU->>CPU: 发现预测错误,丢弃结果 Note over CPU: 但缓存中留下了痕迹! 攻击者->>缓存: Flush+Reload 探测缓存状态 缓存-->>攻击者: 推断出敏感数据

8.2 缓解措施#

措施机制性能影响
IBRS(Indirect Branch Restricted Speculation)限制间接分支预测5-30%
STIBP(Single Thread Indirect Branch Predictors)隔离超线程的预测器5-15%
Retpoline用返回指令替代间接跳转0-10%
Microcode 更新硬件级别的预测器隔离0-5%
Note

Spectre 攻击揭示了推测执行的一个根本矛盾:性能需要推测,安全需要限制推测。后续所有 CPU 微架构更新都在这个矛盾中寻找平衡。在第 5 章:乱序执行中更深入地讨论推测执行的安全影响。

九、动手实验#

9.1 实验 1:测量分支预测失败率#

# 编译并运行排序实验
gcc -O2 -o branch_test branch_test.c
perf stat -e branches,branch-misses ./branch_test
# 对比排序前后的 branch-misses 比例

9.2 实验 2:likely/unlikely 的效果#

#include <stdio.h>
#include <time.h>
#define N 1000000000
int main() {
int sum = 0;
// likely 版本(99% 走 if 分支)
clock_t start = clock();
for (long i = 0; i < N; i++) {
if (__builtin_expect(i >= 0, 1)) { // 几乎总是 true
sum += i;
}
}
clock_t end = clock();
printf("likely: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
// unlikely 版本(99% 走 if 分支,但标记为 unlikely)
sum = 0;
start = clock();
for (long i = 0; i < N; i++) {
if (__builtin_expect(i >= 0, 0)) { // 标记为 unlikely(实际几乎总是 true)
sum += i;
}
}
end = clock();
printf("unlikely: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
return 0;
}

9.3 实验 3:观察 BTB 容量#

#include <stdio.h>
#include <time.h>
// 测试不同数量的分支对预测准确率的影响
#define BRANCH_COUNT 4096
int main() {
// 创建一个包含大量分支的跳转表
void *targets[BRANCH_COUNT];
for (int i = 0; i < BRANCH_COUNT; i++) {
targets[i] = &&label + i; // 简化示意
}
// 测量间接跳转的预测准确率
// 当跳转表大小超过 BTB 容量时,准确率会骤降
// ...
return 0;
}

十、小结#

上一章剖析了指令流水线与冒险处理。

概念要点对软件的影响
2-bit 饱和计数器基础预测器,需要连续两次错误才改变循环通常预测准确
GShare利用全局历史信息相关分支预测更准
混合预测器多个预测器竞争选择整体准确率 >97%
BTB缓存分支目标地址间接跳转可能 BTB 未命中
RAS函数返回地址栈深度递归可能溢出
预测失败惩罚15-20 周期3% 失败率 → 8% 性能损失
Spectre推测执行的缓存侧信道安全与性能的权衡

下一步乱序执行与推测执行——CPU 如何突破指令顺序的限制?寄存器重命名如何消除假依赖?Spectre/Meltdown 的技术根源是什么?

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

分支预测:如果猜错了代价多大
https://blog.souloss.com/posts/cpu-architecture/branch-prediction/
作者
Souloss
发布于
2026-03-05
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时