流水线的基本实现
上一节讲清了流水线的思想。这一节把它落到具体的五段上:每一段做什么、段与段之间靠什么衔接、不同类型的指令在各段上分别干什么。
这一节的表是 5.6.3 冒险分析的前提——只有先知道”写回发生在第 5 段、读寄存器发生在第 2 段”,才能算出数据冒险要停几拍。
机制
经典五段
| 段 | 名字 | 做什么 | 用到的部件 |
|---|---|---|---|
| IF | 取指 | 按 PC 取指令,PC 自增 | 指令存储器、PC 加法器 |
| ID | 译码 / 取数 | 译码,同时按固定位置的寄存器号读寄存器堆 | 译码器、寄存器堆读端口 |
| EX | 执行 | ALU 运算;或计算访存地址;或计算转移目标 | ALU |
| MEM | 访存 | 读写数据存储器(只有 Load/Store 用) | 数据存储器 |
| WB | 写回 | 把结果写入寄存器堆 | 寄存器堆写端口 |
四条必须记住的结构事实:
- ID 段读寄存器,WB 段写寄存器——两者相隔三个段,这是所有数据冒险的根源
- EX 段的 ALU 被三种用途共享:算术运算、访存地址计算(基址+偏移)、转移目标计算
- MEM 段只有访存指令真正在用,运算指令这一段是空转的
- IF 段和 MEM 段都要访问存储器——所以必须是两个独立的存储器(指令存储器 + 数据存储器),否则每拍都结构冲突
第 4 条就是流水线 CPU 采用哈佛结构的原因(5.2.3 讲单周期方案时已经遇到过同一个问题)。现代处理器靠分离的 L1 指令 Cache 和 L1 数据 Cache 来满足它(3.5.2)——这是”为什么 L1 要分成 I-Cache 和 D-Cache”的真正答案,不是因为指令和数据的局部性不同。
为什么运算指令也要走 MEM 段
ADD R1, R2, R3 在 MEM 段什么都不做,却仍然要占用这一拍。
理由是流水线必须整齐:如果允许运算指令跳过 MEM 段直接进 WB,那么
I1: ADD IF ID EX ----WB ← 跳过 MEM,第 4 拍写回
I2: LW IF ID EX MEM WB ← 第 5 拍写回
看起来省了一拍,但两条指令可能在同一拍抢占寄存器堆的写端口(若 I2 提前一条则冲突),而且指令的完成顺序会乱——先发射的指令后完成,异常发生时无法保证精确异常。
所以”让快的指令陪着慢的一起走完”,换来的是写端口不冲突和完成顺序不乱。 这是本章那条”要整齐就得按最慢的来”的第五次出现,而这一次付出的代价不是时间浪费,是一个空转的段。
段间寄存器
每两段之间插一个寄存器,存放上一段传给下一段的全部信息。
段间寄存器里存什么,比它存在这件事更值得想清楚。
| 寄存器 | 存放 |
|---|---|
| IF/ID | 取回的指令、PC+4 |
| ID/EX | 读出的两个操作数、立即数、目的寄存器号、本条指令的控制信号 |
| EX/MEM | ALU 结果、待写入内存的数据、目的寄存器号、剩余的控制信号 |
| MEM/WB | 从内存读出的数据或 ALU 结果、目的寄存器号、写回控制信号 |
“控制信号也要跟着一起往下传”是最容易忽略的一条。 控制器在 ID 段一次性算出这条指令在后面所有段要用的全部控制信号,然后让它们随着指令一起在流水线里往下走——因为等到 MEM 段时,IR 里装的已经是三条之后的指令了。
这解决了一个看起来矛盾的问题:5.4.1 说控制信号由”操作码 + 节拍”决定,但流水线里同一拍有五条不同的指令在跑,一个 IR 显然不够用。答案是:控制信号不再按”节拍”生成,而是在 ID 段一次生成、随指令流动。 五个段各自取用属于自己的那一份。
目的寄存器号也必须一路带下去——WB 段要往哪个寄存器写,这个信息来自 ID 段的指令,中间隔了三拍,只能靠段间寄存器传递。
各类指令在五段上的动作
| 段 | 运算类 ADD R1,R2,R3 | 取数 LW R1,8(R2) | 存数 SW R1,8(R2) | 分支 BEQ R1,R2,L |
|---|---|---|---|---|
| IF | 取指、PC+4 | 同 | 同 | 同 |
| ID | 读 R2、R3 | 读 R2 | 读 R2、R1 | 读 R1、R2 |
| EX | ALU 算 R2+R3 | ALU 算 R2+8 | ALU 算 R2+8 | ALU 比较 / 算目标地址 |
| MEM | 空转 | 读内存 | 写内存 | (部分设计在此改 PC) |
| WB | 写 R1 | 写 R1 | 空转 | 空转 |
三点值得盯住:
SW在 ID 段要读两个寄存器——一个是地址基址 R2,一个是待存的数据 R1。这一点常被忽略,但它决定了SW也会遇到数据冒险。SW和BEQ没有 WB 段的动作——它们不写寄存器,所以永远不会成为数据冒险的”生产者”。- 分支指令在哪一段确定是否跳转,决定了控制冒险的代价。经典五段设计里在 EX 段(或优化到 ID 段)——越早确定,浪费的拍数越少,见 5.6.3。
段数与
取最慢段,加上段间寄存器开销。 这个式子有两个直接推论:
- 分段要尽量均匀——某一段特别慢,其余段全部陪跑
- 段数不能无限增加——
是固定开销,段分得越细,它在 里占的比例越大
第二条在 5.6.4 会算出具体的边界,也是”超流水线”这个技术的天花板所在(5.6.5)。
边界
边界辨析:ID 段读寄存器 / WB 段写寄存器
相隔三个段,这是数据冒险的全部根源:
I1: IF ID EX MEM WB ← 第 5 拍才写 I2: IF ID ... ← 第 3 拍就要读I2 在第 3 拍读到的是旧值。停顿几拍、能否转发,全靠数这个距离,见 5.6.3。
边界辨析:为什么必须有两个存储器
IF 段和 MEM 段在同一拍分别访存,共用一个存储器必然冲突。
解决方式是分离的指令 Cache 和数据 Cache。这才是 L1 分 I/D 的首要理由——不是局部性差异,是结构冒险。
边界辨析:控制信号在流水线里怎么走
在 ID 段一次性生成,随指令流经段间寄存器往下传。
不是每段现算——因为流水线里同一拍有五条不同指令,一个 IR 无法同时服务五个段。
边界辨析:MEM 段空转不能省
运算指令跳过 MEM 会造成写端口冲突和完成顺序错乱,后者会破坏精确异常。
整齐比省一拍更重要。
对照速查
| 段 | 关键动作 | 谁用 |
|---|---|---|
| IF | 取指、PC+4 | 全部 |
| ID | 译码 + 读寄存器 | 全部 |
| EX | ALU:运算 / 算地址 / 算转移目标 | 全部 |
| MEM | 访存 | 只有 Load/Store |
| WB | 写寄存器 | 运算类、Load |
| 指令 | 写寄存器? | 访存? | 会不会成为数据冒险的”源头” |
|---|---|---|---|
ADD | ✅ WB | ❌ | ✅ |
LW | ✅ WB | ✅ 读 | ✅ 且最难处理 |
SW | ❌ | ✅ 写 | ❌ |
BEQ | ❌ | ❌ | ❌ |
考点
- 五段各做什么:默写题。ID 段读寄存器、WB 段写寄存器是重中之重。
- 为什么要两个存储器:结构冒险,答分离的 I-Cache / D-Cache。
- 段间寄存器存什么:数据 + 目的寄存器号 + 控制信号。后两项最常漏。
的取法:最慢段 + 段间寄存器延迟。 - 哪些指令写寄存器:判断数据冒险时的第一步筛查。
链接
- 🏠 返回总览:计算机组成原理第 5 章:中央处理器总览
- ⬅️ 上一节:5.6.1 指令流水线的基本概念
- ➡️ 下一节:5.6.3 流水线的冒险与处理
- 🔗 专用数据通路:5.3.3 数据通路的基本结构
- 🔗 I-Cache 与 D-Cache:3.5.2 Cache 的基本工作原理
- 📖 名词库:第 5 章名词库