把指令执行拆成若干独立阶段、让不同指令同时处于不同阶段:周期短(多周期优点)+ 吞吐≈1 IPC(单周期理想),代价是三类冒险。
经典 RISC 五级:
IF → ID → EX → MEM → WB
取指 译码 执行 访存 写回
| 级 | 缩写 | 干什么 |
|---|---|---|
| 取指 | IF | PC 取指令到 IR,PC+1(或 +4) |
| 译码/读寄存器 | ID | 译 opcode,读 rs1/rs2,符号扩展立即数 |
| 执行 | EX | ALU 运算 / 算有效地址 |
| 访存 | MEM | 仅 load/store 用,读写数据内存 |
| 写回 | WB | 结果写回寄存器文件 |
周期: 1 2 3 4 5 6 7 8
指令1 IF ID EX MEM WB
指令2 IF ID EX MEM WB
指令3 IF ID EX MEM WB
指令4 IF ID EX MEM WB
稳态每周期完成一条指令(IPC≈1),五条同时在场各占一级。总时间 ≈ (k+n-1) 周期(k 级、n 条),比单周期 k×n 快近 k 倍。
对比:单周期周期长、吞吐 1;多周期周期短但吞吐 1/k;流水线周期短且吞吐≈1。本质是把关键路径切成 k 段、每段配锁存器,牺牲锁存器面积和冒险停顿。
流水线要求每条指令语义与顺序执行一致,违反即冒险:
两条指令同时要用同一硬件资源。典型:单存储器机里 IF 取指与 MEM 访存同时访内存。解法:分离指令/数据 Cache、寄存器文件「前半个周期写+后半个周期读」、加多端口。
后一条指令要用前一条还没写回的结果:
| 类型 | 含义 | 危险 |
|---|---|---|
| RAW | 读后写(真依赖) | 后读旧值,必须处理 |
| WAR | 写后读(反依赖) | 顺序执行不出现,乱序才出现 |
| WAW | 写后写(输出依赖) | 同上,乱序才出现 |
顺序流水线只需处理 RAW(WAR/WAW 是乱序执行的课题)。典型:
add r1, r2, r3 ; r1 在 EX 算出,WB 才写回
sub r4, r1, r5 ; ID 阶段就要读 r1 —— 但 r1 还没写回!
分支/jump 改 PC,但取指在分支结果算出前就要决定取哪。解法:分支预测(6.4)。
转发(旁路):数据在 ALU 一算出、还没写回寄存器时,直接从 EX/MEM 或 MEM/WB 级间锁存器「偷」出来,绕过寄存器文件直接喂给下一条的 EX——解决大多数 RAW,几乎零代价。
add r1, r2, r3 EX 算出 r1 ──┐
sub r4, r1, r5 EX 需要 r1 ◄─┘ 直接旁路,不经过 WB
停顿(stall / 插入气泡):有些 RAW 转发救不了,最典型是 load-use:
ld r1, 0(r2) ; r1 要到 MEM 结束才知道
add r3, r1, r4 ; EX 就需要 r1,转发也来不及
须插入一个气泡让 add 等一周期。编译器用指令调度把无关指令塞进 load 与 use 之间填掉气泡。
对比:转发用「绕路」提前送数据、不损吞吐;停顿用「等待」硬扛、损一周期。取舍看「数据最早在哪级可用」:EX 之后可转发,load 的 MEM 才可用就只能停。
核心是「赌一个方向,赌错就冲刷」。
静态预测:编译时定死——总预测不跳(BTFN),或编译器用 BTB 填提示位。
动态预测:运行时用历史:
预测不跳 预测跳
│ 错 错 │
┌────┴──┐ ┌────┴──┐
不跳 00 ──▶ 01 11 ◀── 10 跳
▲ ◀──正确──▶ ▲
└──────── 连续错两次才跨越 ───────┘
注意:预测错代价 = 冲刷已进 IF/ID/EX 的错误指令,损失 k 周期(k≈流水线深度)。现代 CPU 用更深的流水线换主频,代价是预测错更贵,故分支预测器成了性能隐形发动机——也是 Spectre 能从预测器侧信道偷数据的原因。
for 里 if 稳定,预测率高);if 条件随机则预测率崩到 50%、性能骤降——同 O(n) 代码快慢差几倍的常见来源。