流水线的基本实现

上一节讲清了流水线的思想。这一节把它落到具体的五段上:每一段做什么、段与段之间靠什么衔接、不同类型的指令在各段上分别干什么。

这一节的表是 5.6.3 冒险分析的前提——只有先知道”写回发生在第 5 段、读寄存器发生在第 2 段”,才能算出数据冒险要停几拍。

机制

经典五段

段名字做什么用到的部件
IF取指按 PC 取指令,PC 自增指令存储器、PC 加法器
ID译码 / 取数译码,同时按固定位置的寄存器号读寄存器堆译码器、寄存器堆读端口
EX执行ALU 运算;或计算访存地址;或计算转移目标ALU
MEM访存读写数据存储器(只有 Load/Store 用)数据存储器
WB写回把结果写入寄存器堆寄存器堆写端口

四条必须记住的结构事实:

  1. ID 段读寄存器,WB 段写寄存器——两者相隔三个段,这是所有数据冒险的根源
  2. EX 段的 ALU 被三种用途共享:算术运算、访存地址计算(基址+偏移)、转移目标计算
  3. MEM 段只有访存指令真正在用,运算指令这一段是空转的
  4. 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/MEMALU 结果、待写入内存的数据、目的寄存器号、剩余的控制信号
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
EXALU 算 R2+R3ALU 算 R2+8ALU 算 R2+8ALU 比较 / 算目标地址
MEM空转读内存写内存(部分设计在此改 PC)
WB写 R1写 R1空转空转

三点值得盯住:

  1. SW 在 ID 段要读两个寄存器——一个是地址基址 R2,一个是待存的数据 R1。这一点常被忽略,但它决定了 SW 也会遇到数据冒险。
  2. SW 和 BEQ 没有 WB 段的动作——它们不写寄存器,所以永远不会成为数据冒险的”生产者”。
  3. 分支指令在哪一段确定是否跳转,决定了控制冒险的代价。经典五段设计里在 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译码 + 读寄存器全部
EXALU:运算 / 算地址 / 算转移目标全部
MEM访存只有 Load/Store
WB写寄存器运算类、Load
指令写寄存器?访存?会不会成为数据冒险的”源头”
ADD✅ WB❌✅
LW✅ WB✅ 读✅ 且最难处理
SW❌✅ 写❌
BEQ❌❌❌

考点

  • 五段各做什么:默写题。ID 段读寄存器、WB 段写寄存器是重中之重。
  • 为什么要两个存储器:结构冒险,答分离的 I-Cache / D-Cache。
  • 段间寄存器存什么:数据 + 目的寄存器号 + 控制信号。后两项最常漏。
  • 的取法:最慢段 + 段间寄存器延迟。
  • 哪些指令写寄存器:判断数据冒险时的第一步筛查。

链接