CSAPP - 07.PIPE-01
主要内容:
本章重点介绍了从 SEQ (顺序执行) 处理器向 PIPE (流水线) 处理器的演进过程。
- 流水线原理:流水线 (Pipelining) ——将任务分解为子任务并行处理,以提高 吞吐量 (Throughput)。
- PIPE- 设计:在 SEQ+ 的基础上插入流水线寄存器 (Pipeline Registers),实现了基本的流水线处理器 PIPE-。
- 流水线挑战:讨论了流水线带来的新问题,主要是 数据冒险 (Data Hazards) 和 控制冒险 (Control Hazards),以及通过插入 NOP 指令这种低效的解决方法。
- ISA(指令集)没有改变
- 无论是 SEQ 还是 PIPE,它们支持的指令集 (Y86-64) 是完全一样的。
- 程序员写出的汇编代码
addq %rax, %rbx在两者上都能跑,结果也一样。
- 改变的是微架构(Microarchitecture)
- 硬件电路结构变了:我们在 SEQ 的基础上,在各个阶段之间插入了流水线寄存器 (Pipeline Registers)。
- 执行方式变了:
- SEQ:一条指令必须跑完所有 6 个阶段(
F->D->E->M->W),下一条指令才能进场。 - PIPE:第一条指令刚跑完 F 阶段进入 D 阶段,第二条指令就可以马上进入 F 阶段。
- SEQ:一条指令必须跑完所有 6 个阶段(
Motivation:
现代 CPU 为了追求高性能,普遍采用深度流水线技术。理解流水线的设计原理、局限性(如不均匀延迟、寄存器开销)以及它带来的冒险问题,是深入理解现代计算机体系结构性能瓶颈的关键。
1. 流水线的基本原理 (General Principles of Pipelining)
1.1 现实世界的流水线
- 例子:洗车店、福特汽车装配线。
- 核心思想:
- 将整个处理过程划分为若干个独立的阶段 (Independent Stages)。
- 让对象(如汽车、指令)依次通过这些阶段。
- 并行处理:在任何时刻,多个对象正在被不同的阶段同时处理。
1.2 计算系统中的流水线示例
假设一个组合逻辑电路需要 300ps (皮秒) 完成计算,加上寄存器加载需要 20ps。
-
非流水线系统 (Unpipelined System):
- 延迟 (Delay):320 ps (300 + 20)。
- 吞吐量 (Throughput):1 / 320ps ≈ 3.12 GIPS (十亿指令/秒)。
- 特点:必须等上一条指令完全做完,下一条才能开始。
-
3级流水线系统 (3-Way Pipelined System):
- 将组合逻辑拆分为 A, B, C 三个阶段,每个 100ps。
- 插入流水线寄存器 (20ps)。
- 单阶段时间:120 ps (100 + 20)。
- 吞吐量:1 / 120ps ≈ 8.33 GIPS。
- 延迟:3 * 120 = 360 ps (单条指令的总延迟变长了)。
- 优势:吞吐量提升了 2.67 倍。
1.3 流水线的局限性 (Limitations)
- 不均匀的阶段延迟 (Nonuniform Delays):
- 流水线的时钟周期受限于最慢的那个阶段。
- 如果阶段划分不均匀(例如 50ps, 150ps, 100ps),吞吐量会被 150ps 的阶段拖慢,其他快阶段会空闲。
- 寄存器开销 (Register Overhead):
- 每个流水线寄存器都会引入延迟(如 20ps)。
- 随着流水线级数加深,寄存器延迟占总周期的比例会越来越大,从而限制了流水线深度的上限。
数据依赖 (Data Dependency)
- 定义: 数据依赖指的是指令之间关于数据的先后使用关系。当一条指令需要使用前一条(或几条)指令计算出的结果作为操作数时,这两条指令之间就存在数据依赖。
- 类型:
- 读后写 (Read-After-Write, RAW): 这是最常见也是流水线中主要关注的依赖。指令 B 需要读取指令 A 写入的寄存器或内存数据。如果 A 还没写完,B 就读了,就会出错。这被称为“真依赖 (True Dependency)”。
- 写后读 (Write-After-Read, WAR): 指令 B 写入指令 A 读取的位置。在乱序执行中需要注意,但在简单的顺序流水线中通常不是大问题。
- 写后写 (Write-After-Write, WAW): 指令 B 和指令 A 写入同一个位置。需要保证最终结果是 B 的值。
- 本质: 它是程序逻辑的固有属性,由代码本身的顺序决定。
数据冒险 (Data Hazard)
- 定义: 数据冒险是指由于数据依赖的存在,导致流水线中的下一条指令无法在预期的时钟周期内正确执行的现象。简单来说,就是“想用的数据还没准备好”。
- 发生场景:
- 指令 修改了寄存器 。
- 紧接着的指令 需要读取寄存器 。
- 在流水线中,指令 可能还在执行 (Execute) 或 写回 (Write Back) 阶段,数据还没真正写入寄存器文件。
- 此时指令 在 译码 (Decode) 阶段尝试读取 ,读到的是旧值(脏数据)。
- 本质: 它是流水线硬件设计导致的时间冲突问题。
2. 构建流水线 Y86-64 处理器 (Creating a Pipelined Y86-64 Processor)
2.1 改造 SEQ -> SEQ+
为了适应流水线,首先需要对 SEQ 进行微调,得到 SEQ+:
- PC 阶段的移动:将 PC 更新阶段移到周期的开始。
- 目的:为了在流水线中更早地计算出下一条指令的地址。
- PC 预测:不再通过寄存器保存 PC,而是基于上一条指令的信息动态计算当前 PC。
2.2 插入流水线寄存器 (Inserting Pipeline Registers)
在 SEQ+ 的各个阶段之间插入流水线寄存器,形成 PIPE- 架构。这些寄存器用于保存指令在各阶段产生的中间结果,使其能传递到下一阶段。
虽然名字里有“寄存器”三个字,但请不要把它和“通用寄存器”(如 %rax, %rbx, %rsp 组成的 Register File)混淆。
- 通用寄存器 (Register File): 程序员可见的,用来存代码里的变量。
- 流水线寄存器: 程序员不可见的,是 CPU 内部用来存“指令状态”的。
- 硬件实现: 它们本质上是一组 D 触发器 (D Flip-Flops)。这意味着它们是由时钟信号 (Clock) 控制的。
流水线寄存器 (Pipeline Registers)
它们是位于 F, D, E, M, W 各个阶段之间的硬件“隔离墙”(由时钟控制的触发器组成)。
核心作用
- 隔离:防止当前阶段不稳定的电信号干扰后续阶段。
- 传递:在时钟上升沿,将上一阶段的计算结果“锁存”,带入下一阶段,防止被紧跟的新指令覆盖。
命名规则:为什么要有前缀?
- 无前缀 (如 valE):组合逻辑输出 (Wire)。代表当前阶段(Execute)ALU 刚刚算出的瞬时值。
- 有前缀 (如 M_valE):流水线寄存器输出 (State)。代表该数值目前存储在 M 寄存器中,供当前阶段(Memory)使用或透传。
数据流动实例 (以 valM 为例)
- Memory 阶段:从内存读出数据 产生瞬时信号 valM。
- 时钟跳变:信号被锁存进 W 寄存器。
- Write Back 阶段:W 寄存器输出 W_valM 最终写入通用寄存器文件。
流水线寄存器命名规范:
- F (Fetch): 保存 PC 预测值。
- D (Decode): 位于取指和译码之间。
- E (Execute): 位于译码和执行之间。
- M (Memory): 位于执行和访存之间。
- W (Write back): 位于访存和写回之间。
- SEQ (顺序) 模式 —— “独占”
- 场景:CPU 里同一时刻只有一条指令在运行。
- 状态:当这一条指令在执行时,整个电路中的
icode(指令代码)、valC(立即数)、dstE(目标寄存器)等信号线,传输的都是这一条指令的数据。 - 命名:因为没有竞争者,我们直接叫
icode就行,不会产生歧义。
- PIPE (流水线) 模式 —— “并发”
- 场景:CPU 里同一时刻有 5 条不同的指令 在同时运行,分别处于 F, D, E, M, W 五个阶段。
- 冲突:
- 取指阶段 (F) 正在读取指令 A(例如
jmp)。 - 译码阶段 (D) 正在解析指令 B(例如
addq)。 - 执行阶段 (E) 正在计算指令 C(例如
mrmovq)。
- 取指阶段 (F) 正在读取指令 A(例如
- 问题:这三条指令都有自己的
icode。如果你在控制逻辑里只写一个icode,电路根本不知道你指的是哪条指令。是指正在取指的jmp?还是正在译码的addq?
信号命名:
- 为了区分不同阶段的同名信号,加上阶段前缀。
- 例如:
D_icode(译码阶段的icode),E_icode(执行阶段的icode),M_valE(访存阶段的valE)。
2.3 PC 预测策略 (PC Prediction Strategy)
在 PIPE 的电路图中(特别是在 Fetch 阶段的开头),你会看到一个叫 Select PC 的逻辑块。它取代了 SEQ 中最后的“更新 PC”阶段。
| 特性 | SEQ 处理器 | PIPE 处理器 |
|---|---|---|
| PC 何时更新 | 当前指令结束时 (第 6 阶段) | 下一条指令开始前 (第 1 阶段) |
| 决策依据 | 事实 (确切知道下一条去哪) | 预测 (先猜一个,猜错再改) |
| 电路位置 | 这里的逻辑是计算 newPC |
这里的逻辑叫 Select PC |
| PC 寄存器 | 真实的硬件寄存器 PC | F 流水线寄存器 里的 predPC |
所谓的“预测”,就是当 CPU 刚刚把当前指令(比如一条跳转指令)读进来,还完全不知道这条指令想干什么的时候,就强行猜一个地址
- 如果猜对了,流水线继续飞速运行。
- 如果猜错了,就需要把刚才读进来的东西扔掉,重新去正确的地方读(这就是“预测失败的惩罚”)。
- 策略:
- 普通指令:预测为
valP(下一条顺序指令)。 Call / Jmp(无条件):预测为valC(目标地址)。JXX(条件跳转):总是预测选择分支 (Always Taken),即预测为valC。- Ret:不预测,因为返回地址在栈中,必须等到访存阶段读出后才知道。这会导致流水线暂停。
- 普通指令:预测为
4. 流水线冒险 (Pipeline Hazards)
PIPE- 虽然实现了流水线结构,但它面临严重的依赖 (Dependency) 问题。
4.1 数据冒险 (Data Hazards)
当一条指令需要使用上一条(或上几条)指令尚未写回的结果时,就会发生数据冒险。
- 根本原因:
Read-after-write (RAW)依赖。下一条指令在译码阶段读取寄存器时,上一条指令的结果还在执行或访存阶段,还没写回寄存器文件。 - 例子:
1 | |
4.2 控制冒险 (Control Hazards)
当处理器无法根据当前指令确定下一条指令的地址时(通常发生在跳转或返回指令),就会发生控制冒险。
- 根本原因:PC 的更新依赖于指令执行的结果,而流水线已经取出了后续的指令。
- 例子:
- 条件跳转 (Mispredicted Branch):预测跳转了,但实际没跳。导致流水线中取入了错误的指令。
- Ret 指令:返回地址必须等到访存阶段读栈后才知道,导致后面几条指令取指错误。
4.3 临时解决方案:插入 NOP
在 PIPE- 中,解决冒险的简单(但低效)方法是编译器或硬件自动插入 NOP (No Operation) 指令。
- 原理:通过插入 NOP 产生气泡,推迟后续指令的执行,直到依赖的数据准备好。
- 缺点:严重降低性能(吞吐量)。如果一条指令后面要跟 3-4 个 NOP,流水线的优势就荡然无存了。
4.4 总结与展望
- PIPE-:这是一个通过插入流水线寄存器构建的基础流水线处理器。
- 问题:无法高效处理数据冒险和控制冒险。
- 下一步 (PIPE):我们将通过 数据转发 (Data Forwarding) 和更复杂的控制逻辑来解决这些问题,构建最终的高效 PIPE 处理器。