CS152 · 第 4 课

Pipelining I:Iron Law 与经典 5 级流水线

第 3 课已经证明"复杂指令集会阻碍并行和流水线实现"。这一课正式引入贯穿全课程的性能分析框架——Iron Law——然后用经典的 5 级 RISC 流水线(Fetch/Decode/Execute/Memory/Writeback)把它落地成具体电路,并开始讨论指令之间在流水线里如何互相干扰(hazard)。

授课:Sophia Shao 预计用时:55 分钟 难度:中等

回顾第 3 课


处理器性能的"铁律"(Iron Law)

本课程反复使用的核心性能分解公式:

\[ \frac{\text{Time}}{\text{Program}} = \frac{\text{Instructions}}{\text{Program}} \times \frac{\text{Cycles}}{\text{Instruction}} \times \frac{\text{Time}}{\text{Cycle}} \]

微架构CPI时钟周期
微编码(Microcoded)>1短
单周期不流水(Single-cycle unpipelined)1长
流水线(Pipelined)1短

这张表正是三种设计风格的"一句话总结":微编码用短周期换来了灵活性,但代价是 CPI>1;单周期不流水的机器 CPI=1,但每个周期必须长到能容纳最慢的指令(例如访存),周期时间被拖累;流水线设计的目标,是同时拿到"CPI≈1"和"短周期"——这正是它相对前两种风格的根本优势所在。


三种机器的 CPI 对比(含数字重新验算)

假设执行 3 条指令:

一个常见的直觉误区:"5 级流水线的 CPI 是不是等于 5?" ——不是!CPI 衡量的是"稳态下平均每条指令消耗多少个周期",而不是"一条指令要经过多少个流水级"。只要流水线能够每个周期发射一条新指令、没有被打断,CPI 就趋近于 1,无论流水级数是 5 级还是 500 级。这是本课程中最容易混淆、也最重要的一个概念澄清。


经典 5 级 RISC 流水线

把单周期数据通路按功能切成 5 段,每段之间插入流水线寄存器:

阶段缩写主要工作
Fetch(取指)F用 PC 访问指令缓存,取出指令
Decode(译码)D译码指令、从寄存器堆读出操作数 A/B、生成立即数 Imm
Execute(执行)XALU 计算
Memory(访存)M访问数据缓存(load/store)
Writeback(写回)W把结果写回寄存器堆

这一版设计假设寄存器堆/存储器是"同步写、异步读"的:写入在时钟边沿完成,读出则是纯组合逻辑,同一周期内写入的值不能被同一周期的读出直接看到(这个假设直接决定了后面 hazard 的产生方式)。


指令在流水线中会互相干扰:Hazard 分类

当多条指令同时存在于流水线的不同阶段时,可能出现三类冲突(hazard):

处理这些冒险通常需要在流水线中插入气泡(bubble),使理想情况下 CPI=1 的流水线实际 CPI > 1。

气泡如何影响实际 CPI(重新验算):若 3 条指令原本能在 3 个周期内完成(CPI=1),插入 1 个气泡后变成 4 个周期完成(CPI=4/3≈1.33),插入 2 个气泡后变成 5 个周期完成(CPI=5/3≈1.67)。这里 CPI 的计算方式是:从第一条指令完成的那一刻,到序列中最后一条指令完成的那一刻,所经过的周期数除以指令数。

结构冒险的解决

结构冒险发生在两条指令同时需要同一个硬件资源时。硬件上可以通过让较新的指令在原地等待(stall)来解决;也永远可以通过增加硬件资源来彻底避免(例如给内存增加第二个访问端口)。经典 RISC 5 级整数流水线在设计上没有结构冒险——但许多真实 RISC 实现在乘法器、除法器、浮点单元等多周期部件上,以及寄存器写回端口上,仍然可能出现结构冒险。

数据冒险的三种类型

考虑一串寄存器-寄存器指令 r_k ← r_i op r_j:

类型例子名称
数据依赖r3 ← r1 op r2
r5 ← r3 op r4
写后读(Read-after-Write, RAW)
反依赖r3 ← r1 op r2
r1 ← r4 op r5
读后写(Write-after-Read, WAR)
输出依赖r3 ← r1 op r2
r3 ← r6 op r7
写后写(Write-after-Write, WAW)

解决数据冒险的三种策略

Interlock vs. Bypass 的具体对比(add x1,x3,x5 后紧跟 sub x2,x1,x4):

纯用互锁:sub 需要在译码阶段插入 2 个气泡,等到 add 的写回阶段完成、寄存器堆里确实有更新的值之后才能真正译码——五级流水线因此退化成实际上更长的执行时间。

加上从 ALU 输出直接到 ALU 输入的旁路路径后:sub 在自己的执行阶段直接拿到 add 刚刚算出、还未写回的结果,完全不需要气泡,两条指令背靠背顺畅执行。这正是现代流水线几乎必然采用"全旁路数据通路"(fully bypassed datapath)的原因——把执行、访存、写回阶段的结果都直接接回译码阶段的操作数选择器。

数据值的推测(value speculation)目前只在少数场景下被证明有效:分支预测、栈指针更新、访存地址消歧——这几个话题会在后续课程(分支预测、乱序执行)中展开。


控制冒险:如何计算下一条 PC

不同类型的控制流指令,需要在流水线的不同阶段才能确定下一条指令地址:

指令类型计算下一个 PC 需要的信息
无条件跳转(Unconditional Jump)opcode、PC、偏移量——译码阶段即可确定
寄存器跳转(Jump Register)opcode、寄存器值、偏移量——要等到执行阶段读出寄存器值
条件分支(Conditional Branch)opcode、(用于判断条件的)寄存器、PC、偏移量——同样要等到执行阶段算出条件
其他所有指令opcode、PC(还需要确认它不属于以上几类)

RISC-V 中,无条件的 PC 相对跳转(如 j target)在译码阶段就能确定跳转目标,因此只需要杀掉(kill,转换成气泡)译码阶段之后已经被错误取出的 1 条指令;而条件分支和寄存器跳转要等到执行阶段才能确定目标或跳转与否,因此需要杀掉两条已经被取出/译码的指令,产生 2 个气泡。

三种情形的流水线气泡数(重新核对):

历史设计:分支延迟槽(Branch Delay Slot)

早期 RISC 借鉴了流水化微码引擎的思路,把 ISA 语义改成"分支/跳转指令之后的那条指令,无论分支是否发生,总是会被执行":

0x100 j target
0x104 add x1, x2, x3   // 无论如何都会被执行
...
0x205 target: xori x1, x1, 7

软件必须用有用的指令填满这个"延迟槽",或者显式插入一条 NOP。这样一来,硬件就不需要为无条件跳转插入气泡——延迟槽指令本来就该执行。

1990 年后的 RISC ISA 大多放弃了延迟槽,原因包括:延迟槽把微架构细节固化进了 ISA 语义(类似 IBM 650 让程序员手动排列磁鼓指令顺序以减少延迟);当延迟槽被 NOP 填充时会增加指令缓存的压力和缺失率;在更深的流水线(例如 30 级流水、每周期发射 4 条指令)中,"只有 1 个延迟槽"这个假设根本无法维持;更重要的是,后续课程会讲到的分支预测大幅降低了对延迟槽的需求。

为什么经典 5 级流水线实际上常常 CPI > 1

凡是带有软件可见延迟槽的机器,编译器可能会插入大量 NOP 指令:NOP 降低了 CPI,却增加了每个程序的指令数——这正好提醒我们 Iron Law 三项之间常常存在此消彼长的权衡,不能只孤立地优化其中一项。


异常与中断的术语与历史

本课程采用的术语约定:

历史上,Babbage 的分析机已经有溢出异常;第一台带陷阱机制的系统是 1951 年的 Univac-I(算术溢出可以触发一段两条指令的修复例程,或者按程序员的选择直接停机;后续的 Univac 1103 于 1955 年加入了外部中断,用于实时采集风洞数据);第一台带 I/O 中断的系统是 1954 年的 DYSEAC——它有两个程序计数器,I/O 信号会触发两者之间的切换,同时它也是第一台支持 DMA(直接内存访问)的系统,甚至是第一台"移动"计算机:整机分装在两辆拖车里(共 20 吨),为美国陆军通信兵团建造。


流水线中的精确异常与提交点

异步中断的标准处理流程:处理器在指令 I_i 处停止当前程序(完成所有 I_i 之前的指令——这就是精确中断,precise interrupt的含义),把 I_i 的 PC 存入专用寄存器 EPC,关闭中断并把控制权转交给运行在监管模式下的中断处理程序。

在 5 级流水线中,不同类型的异常可能同时在不同阶段被检测到(Fetch 阶段的 PC 地址异常、Decode 阶段的非法操作码、Execute 阶段的溢出、Memory 阶段的数据地址异常),还要处理来自外部、随时可能到达的异步中断。解决方案:

把异常处理看作一种"推测执行":预测机制——异常极其罕见,所以简单地"预测不会发生异常"已经非常准确;检查机制——异常在指令执行流水线的末端才被真正检测出来,各类异常各有专门的检测硬件;恢复机制——只有到达提交点才真正写入架构状态,因此提交点之前的部分执行结果可以被直接丢弃,然后清空流水线、跳转到异常处理程序。旁路(bypass)机制则允许后续指令提前使用尚未提交的指令结果,而不必等到它真正写回。


常见误区


知识检查

问题 1:一条经典的 5 级 RISC 流水线,在没有任何冒险的理想情况下,其 CPI 大约是多少?

问题 2:add x1,x3,x5 紧跟 sub x2,x1,x4,如果数据通路提供了从 ALU 输出直接到 ALU 输入的旁路路径,需要插入几个气泡?

问题 3:在支持精确异常的 5 级流水线中,为什么需要把异常标志一直携带到"提交点"(通常是 Memory 阶段)才处理,而不是检测到异常就立刻处理?


本课要点

Iron Law

Time/Program = Instructions/Program × CPI × Cycle Time

CPI ≠ 流水级数

理想流水线 CPI 趋近 1,与流水线深度无关

三类 hazard

结构、数据(RAW/WAR/WAW)、控制冒险

精确异常

提交点之前可安全丢弃,之后才写入架构状态


参考资料与延伸阅读