指令系统与流水线
冯·诺依曼结构
┌──────────┐
│ CPU │ ← 控制器 + 运算器 + 寄存器
└────┬─────┘
│ 总线
┌────┴─────────────────────┐
│ 存储器(内存) │ 指令和数据放一起
└──────────────────────────┘核心特征:存储程序——指令和数据都存在同一个内存里,可以被同样地读写。
冯·诺依曼瓶颈:CPU 和内存之间的带宽是有限的,而 CPU 速度远快于内存 → CPU 经常在等数据。
这就是整个存储层次结构存在的理由。
指令执行
取指-译码-执行循环
1. 取指(Fetch): PC → 内存,取指令
2. 译码(Decode): 解析操作码和操作数
3. 执行(Execute): 运算
4. 访存(Memory): 读写内存(如需)
5. 写回(Writeback): 结果写回寄存器
6. PC += 指令长度一条指令要 5 步,但流水线让多条指令的不同阶段重叠:
周期: 1 2 3 4 5 6 7
指令1: F D E M W
指令2: F D E M W
指令3: F D E M W
↑ 5 个周期后,每个周期完成一条指令(吞吐 5 倍)流水线的三种冒险(Hazard)
| 类型 | 原因 | 解法 |
|---|---|---|
| 结构冒险 | 硬件资源冲突(两条指令同时要访存) | 多端口存储器、分离指令/数据缓存 |
| 数据冒险 | 后一条指令需要前一条的结果 | 转发(forwarding)、插入气泡(stall) |
| 控制冒险 | 分支跳转,不知道下一条取哪 | 分支预测、延迟槽 |
数据冒险的例子:
add r1, r2, r3 ; r1 = r2 + r3
sub r4, r1, r5 ; 需要 r1,但 r1 还没写回转发(bypassing):不等写回,直接把 ALU 的输出接到下一条指令的输入。
Load-use 冒险无法完全转发(数据从内存来,太晚):
lw r1, [r2] ; 从内存加载
add r3, r1, r4 ; 必须等一个周期(stall)这就是编译器要做指令调度的原因——在 lw 和 add 之间塞入无关指令。
分支预测
分支错误预测的代价 = 清空流水线 ≈ 15~20 个周期。
预测器演进:
| 类型 | 思路 |
|---|---|
| 静态 | 永远预测不跳转 / 向后跳预测跳转(循环) |
| 2-bit 饱和计数器 | 记录每个分支的历史,连续错两次才改变预测 |
| 局部历史(BHT) | 每个分支自己的历史模式 |
| 全局历史(GShare) | 用全局的分支历史索引 |
| TAGE / 感知机 | 现代高性能预测器,多个历史长度的表 + 投票 |
准确率:现代预测器 > 95%。
对程序员的意义:
// 慢:随机分支,预测失败率 ~50%
for (int i = 0; i < n; i++)
if (data[i] < 128) sum += data[i]; // data 随机
// 快:排序后前一半都小于 128,预测器几乎全对
std::sort(data, data + n);
for (int i = 0; i < n; i++)
if (data[i] < 128) sum += data[i];这个著名的例子:排序后的数组遍历比未排序快 2~3 倍,纯粹因为分支预测。
无分支写法(编译器 -O2 有时会自己做):
sum += (data[i] < 128) ? data[i] : 0; // 可能被编译成 cmov
// 或
sum += data[i] * (data[i] < 128); // 算术消除分支 许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/computer/architecture/instruction-pipeline/
更新于:2026年10月12日