指令流水线
基本概念
如果不对指令的执行过程进行拆分的话,那么指令的执行的粒度则是指令本身,如下图所示:
这样会导致 CPU 的执行效率很低,因为下一条指令必须等待上一条指令完全执行结束后,才能执行下一条指令。
这节谈到的流水线方案就是对以上方式的优化:在 CPU 中将指令的执行过程拆分为多个阶段,每个阶段由不同的部件执行。
物理结构
上图为流水线的 物理结构,指令执行的五个阶段由 CPU 中不同的部件处理,下一阶段的执行部件的执行结果依赖上一个阶段的输入,不同阶段的部件可以并行工作。 这样 CPU 中不同部件的利用率就得到了提高,CPU 执行指令的吞吐也会因此提高。
逻辑结构
当然,上图中包含太多具体的器件,流水线的 逻辑结构 如下图所示:
每个流水段后面都要增加一个流水段寄存器,用于锁存本段处理完的所有数据,以保证本段 的执行结果能在下个时钟周期给下一流水段使。各种寄存器和数据存储器均采 用统一时钟 CLK 进行同步,每来一个时钟,各段处理完的数据都将锁存到段尾的流水段寄存器 中,作为后段的输入。同时,当前段也会收到前段通过流水段寄存器传递过来的数据。
画法
通过指令流水线技术,指令执行的并行粒度从整条指令降低到指令执行的各个阶段。这种细粒度的并行使得不同指令的执行阶段可以重叠(overlap),从而显著缩短所有指令的总体执行时间。
为了清晰地描述流水线的执行过程,通常需要通过图形化的方式来展示指令的执行情况。常见的表示方法有以下两种:
- 常规画法:以横坐标表示时钟周期,纵坐标表示不同的指令。这种方式直观地展示每条指令在各个时钟周期中的执行阶段,便于理解指令间的并行关系和流水线的整体流程。
- 时空图:一种更抽象的表示方法,结合时间和指令的执行阶段,通常以时间为横轴,执行阶段或资源占用为纵轴,展现指令在流水线中的动态流动和阶段重叠情况。
指令执行阶段
一般而言,指令执行包含五个阶段:
- 取指(IF,Instruction Fetch):从指令存储器获取指令并更新程序计数器。
- 译码(ID,Instruction Decode):解析指令、读取源寄存器并生成控制信号。
- 执行(EX,Execute):执行计算或分支逻辑,生成中间结果。
- 访存(MEM,Memory Access):执行内存读写操作,非内存指令为空。
- 写回(WB,Write Back):将结果写回寄存器,非写回指令为空。
采用这五个阶段的流水线也叫做 五阶段流水线。
取指
取指(IF)阶段的主要任务包括:
- 从指令存储器获取指令:根据程序计数器(PC)的值,从指令存储器(Instruction Memory)中读取当前指令。
- 更新程序计数器(PC):增加 PC 的值,指向下一条指令的地址。
- 传递指令:将读取的指令存储到 IF/ID 流水线寄存器,供后续 ID 阶段使用。
译码
译码(ID)阶段的主要任务包括:
- 解析指令:解码指令的操作码和操作数,确定指令类型(如 ADD、LW、BEQ 等)。
- 读取源寄存器:从寄存器文件中读取指令所需的源寄存器值。例如:
- 例如,对于
ADD R1, R2, R3
,需要读取 R2 和 R3 的值。
- 例如,对于
- 生成控制信号:根据指令类型生成后续阶段(如 EX、MEM、WB)所需的控制信号(如 ALU 操作、内存读写、写回使能等)。
执行
执行(EX)阶段的主要任务包括:
- 执行计算操作:根据指令类型,使用算术逻辑单元(ALU)执行计算,如加法、减法、逻辑运算或地址计算。
- 例如,对于
ADD R1, R2, R3
,计算 R2 + R3。
- 例如,对于
- 处理分支条件:对于分支指令,比较寄存器值并确定是否跳转,更新 PC(如果分支预测错误,可能冲刷流水线)。
- 例如,对于
BEQ R1, R2, label
,比较 R1 和 R2 是否相等。
- 例如,对于
- 传递结果:将计算结果(如 ALU 输出或内存地址)存储到 EX/MEM 流水线寄存器,供后续阶段使用。
访存
访存(MEM)阶段的主要任务包括:
- 执行内存操作:对于加载(load)或存储(store)指令,访问数据存储器(Data Memory)以读取或写入数据。
- 对于
MOV R1, 0(R2)
,从地址 R2 + 0 读取内存数据到 R1。
- 对于
- 传递数据:将内存读取的数据(对于加载指令)或 ALU 计算结果(对于非内存指令)存储到 MEM/WB 流水线寄存器。
- 空操作(对于非内存指令):对于非加载/存储指令(如
ADD R1, R2, R3
或BEQ
),MEM 阶段不执行实际操作,仅将 EX 阶段的结果传递到 WB 阶段。
写回
- 将结果写回寄存器:将指令的最终结果(如 ALU 计算结果或加载的内存数据)写入目标寄存器。
- 例如:
ADD R1, R2, R3
将 R2 + R3 的结果写入 R1。
- 例如:
- 空操作(对于无需写回的指令):对于不写回寄存器的指令,WB 阶段不执行实际操作
- 例如,
JUMP label
不包含写寄存器操作。
- 例如,
注意
访存和写回阶段是否一定出现
在标准的五阶段流水线设计中,所有五个阶段在流水线结构上都会“出现”(即指令会按顺序通过这些阶段),但 MEM 和 WB 阶段对于某些指令可能是“空操作”,不执行实际功能。IF、ID 和 EX 阶段则是每条指令都必须执行的实际操作。
访存(MEM)阶段 在流水线结构中存在,但对于非加载/存储指令(如算术、逻辑、分支等),MEM 阶段是空操作,不执行实际内存访问。
写回(WB)阶段 在流水线结构中存在,但对于不需要写回寄存器的指令(如存储、分支、跳转等),WB 阶段是空操作,不执行实际写回。
流水线的冒险和处理
上图中的指令流水线是一种理想情况,然后在实际情况中,情况不会这么简单。指令的流水线执行必须满足两个前提:
第一个前提是指令重叠执行时不会存在任何流水线资源冲突问题,即流水线的各段在同一个时钟周期内不会使用相同的数据通路资源。
第二个前提是指令通过流水线方式指令的结果与串行执行的结果应该相同。
违背以上前提的指令流水线调度方式即发生了 “冒险”,这些冒险总共可以分为三类:
- 第一种是结构冒险,是指令在重叠执行的过程中,硬件资源满足不了指令重叠执行的要求,发生硬件资源冲突而产生的冲突。
- 第二种是数据冒险,是指在同时重叠执行的几条指令中,一条指令依赖于前面指令执行结果数据,但是又得不到时发生的冲突。
- 第三种是控制冒险,它是指流水线中的分支指令或者其他需要改写 PC 的指令造成的冲突。
提示
流水线冒险和冲突
在计算机体系结构中,“冒险”(Hazard)和“冲突”(Conflict)这两个术语在描述流水线执行中遇到的问题时,经常可以互换使用,但它们之间存在细微的差别。更准确地说,“冒险” 是一个更广泛的概念(冒险并不一定出错),而 “冲突” 则表示已经发生了错误。
结构冒险
结构冒险是由于 CPU 的硬件资源有限而引起的。当两条或多条指令需要使用同一硬件资源时,就会发生结构冒险。
上图中画出了不同指令在每个时钟周期所需要使用到的硬件结构,其中指令 0 和指令 1 在第 4 个时钟周期分别需要读和写寄存器,但是 CPU 的架构却并不一定支持这种场景。同样,指令 0 和指令 3 在第 3 个时钟周期分别需要写和读存储器,存储器架构也不一定支持这种场景。若硬件不支持上述场景的话,指令间就发生了结构冒险。
其 处理方法 也很简单,主要分为两种:
- 资源重复:既然结构冒险是资源受限所导致的,我们就增加硬件资源的数量,这样不同的指令在同一个时钟周期就可以去访问不同的硬件资源了。
- 流水线停顿:如果指令 A 和指令 B 发生了结构冒险,那么我们就推迟指令 B 的执行,直到两者不发生结构冒险,如下图所示。
数据冒险
数据冒险是由指令之间的依赖性引起的。一条指令可能需要使用另一条指令的结果,如果这些指令过早地进入流水线,它们可能会尝试在数据准备好之前使用数据。
数据冒险可以分为三类:
- 写后读(RAW, Read After Write):下一条指令的源操作数恰好是上一条指令的目的操作数,正常的逻辑是上一条指令写完该寄存器下一条指令才能读,如果下一条指令在上一条指令写完前就读了,就发生了 RAW 数据冒险。
- 读后写(WAR, Write After Read):下一条指令的目的操作数恰好是上一条指令的源操作数,正常的逻辑是上一条指令读完下一条指令才能写,如果下一条指令在上一条指令读完前就写了,就发生了 RAW 数据冒险。
- 写后写(WAW, Write After Write):两个指令写入同一个数据项,正常的逻辑是下一条指令比上一条指令更晚写,如果出现了相反的情况,就发生了 WAW 数据冒险。
提示
以上的中文名词由于翻译关系可能有些绕,建议大家优先记住英文表示。如果上一条指令叫做 A,下一条指令叫做 B,A 写 B 读并且发生了冒险,就叫做读后写(Read After Write),其他冒险命名以此类推。
关键点在于理解流水线调度执行的结果应该与串行执行相同,如果关于某些数据的 读/写 逻辑出现了与串行执行不一致的地方,就发生了数据冲突。
处理方法
数据冒险的 处理方法 如下所示:
- 流水线停顿(Pipeline Stall):暂停流水线直到数据准备好。
- 数据前推(Data Forwarding):设置相关专用通路,直接将前一条指令的结果传递给需要它的下一条指令,不等结果写回寄存器。
- 重新排序指令(Instruction Reordering):编译器在编译时对指令进行重新排序,以减少数据冒险。
流水线停顿
流水线停顿即当检测到数据冒险时,暂停后续指令(如 I2)的执行,插入“气泡”(bubble,即空操作),让流水线等待,直到依赖的数据(如 I1 的结果)准备好。
假设 I1 和 I2 存在数据冲突,通过流水线停顿,我们可以将 I2 的译码阶段(ID)放到 I1 的写回(WB)之后,这样即可解决数据冲突:
旁路技术
旁路技术也称为数据前递(Data Forwarding),是一种用于解决 RAW(Read After Write)数据冒险的硬件优化技术。通过在流水线阶段之间直接传递数据,旁路技术避免或减少因数据依赖导致的流水线停顿,从而提高流水线效率。
例如:
I1: ADD R1, R2, R3 ; R1 = R2 + R3
I2: SUB R4, R1, R5 ; R4 = R1 - R5
I1 和 I2 存在 RAW 冲突,若使用流水线停顿,需要插入气泡,等待 I1 的 WB 阶段执行完再执行 I2 的 ID 阶段。
若设置相关转发通路,不等前一条指令把计算结果写回寄存器,下一条指令也不再从寄存器读,而将数据通路中生成的中间数据直接转发到 ALU 的输入端。
指令 I1 在 EX 段结束时已得到 R1 的新值,被存放到 EX/MEM 流水段寄存器中,因此可以直接从该流水段寄存器中取出数据返送到 ALU 的输入端,这样,在指令 I2 执行时 ALU 中用的就是 R1 的新值,并且无需等待 I1 完成 WB 阶段。
实例
下面通过一个实际的例子说明在如何在题目中画出 解决了冒险的指令流水线。
假设高级语言一条赋值语句被汇编微如下四条指令:
I1 LOAD R1, [a]
I2 LOAD R2, [b]
I3 ADD R1, R2
I4 STORE R1, [x]
其中 I3
和 I1
之间存在 WAW 数据冒险,
I3
和 I2
之间存在 RAW 数据冒险,
I4
和 I3
之间存在 WAR 数据冒险。
我们可以直接通过流水线停顿解决数据冒险:假设指令 A 和 B 发生了数据冲突,指令 A 在前,指令 B 在后,那么将 B 的 ID 放在 A 的 WB 之后就可以简单粗暴地简单冲突,在考试中画流水线都应采用这种方式。
解决冲突后,四条指令对应的流水线执行如下图所示:
控制冒险
控制冒险是由分支和跳转指令引起的。因为 CPU 需要在执行分支和跳转指令后,才能知道下一条要执行的指令在哪里,这导致了流水线的暂停或者无效的指令进入流水线。
以下举例说明控制冒险是如何发生的:
100: ADD R1, R2, R3 ; R1 = R2 + R3
104: BEQ R1, #0, 200 ; 如果 R1 等于 0,则跳转到地址 200
108: SUB R4, R5, R6 ; R4 = R5 - R6
112: MUL R7, R8, R9 ; R7 = R8 * R9
...
200: OR R10, R11, R12 ; R10 = R11 | R12
在 BEQ 指令的 EX 阶段完成之前,流水线已经开始取下一条指令(地址 108 的 SUB 指令)。问题在于,如果 BEQ 指令的条件成立,应该跳转到地址 200,而不是继续执行地址 108 的指令。 这就产生了控制冒险。
控制冲突的 处理方法 主要包含以下几种:
- 流水线停顿(Pipeline Stall/Bubble): 在条件跳转指令之后,停止后续指令的执行,插入空操作。
- 分支预测(Branch Prediction): 预测分支的结果(跳转或不跳转),并提前取指。如果预测正确,则可以避免停顿;如果预测错误,则需要清空流水线并重新取指。
- 延迟分支(Delayed Branch):编译器或处理器对代码进行优化,将分支指令后的一些不依赖于分支结果的指令先执行,从而减少因分支预测错误造成的开销。
性能指标
吞吐率
流水线的吞吐率(Throughput)是流水线在单位时间内完成的任务数量。
吞吐率 TP 的计算公式为:
$$\text{TP} = \frac{n}{T_k}$$
其中,$n$ 是任务数,$T_k$ 是处理完 $n$ 个任务所用的总时间。
设时钟周期为 $T_c$,流水线的段数为 $k$。在理想无阻塞的情况下,一条 $k$ 段流水线完成 $n$ 个任务需要 $k + n - 1$ 个时钟周期,得出流水线的吞吐率为:
$$\text{TP} = \frac{n}{(k + n - 1) \times T_c}$$
加速比
加速比衡量流水线系统相对于非流水线系统(串行执行)的性能提升。它表示流水线化后完成相同任务所需时间的减少倍数。
假设:
- 非流水线执行 n 个任务的总时间为 $T_{serial} = n \cdot k \cdot T_c$
- 流水线执行 n 个任务的总时间为 $T_{pipeline} = (k + n - 1) \cdot T_c$
加速比 $S$ 定义为
$$S = \frac{T_{serial}}{T_{pipeline}} = \frac{nk}{k + n - 1}$$
在理想情况下,当任务数 $n$ 很大时,可以近似有
$$S \approx k$$
即最大加速比接近流水线阶段数 $k$。
高级流水线
高级流水线通过提升流水线的并行程度来提升流水线的执行效率。指令级并行(ILP)的提升主要有两种策略: 多发射技术 通过多个功能单元并行处理指令,允许一次发射多条指令到流水线; 超流水线技术 通过增加流水线级数,使更多指令在流水线中重叠执行。
以下介绍三种相关技术:超标量流水线、超流水线。
超标量流水线是一种能够在单个时钟周期内并行执行多条指令的处理器设计技术,通过多个并行的执行单元(如ALU、FPU等),处理器可以同时处理多条指令,从而提高指令吞吐量。
指令获取和解码后,处理器动态分析指令之间的依赖关系。如果指令之间没有数据或控制依赖,处理器会将它们分配到不同的执行单元并行执行。
超流水线技术将指令执行过程细分为更多、更小的阶段,从而缩短每个阶段的时间,允许更高的时钟频率。
传统流水线可能有5个阶段(如取指、解码、执行、访存、写回),超流水线可能细分为10个或更多阶段。每个阶段处理时间减少,处理器可以以更高频率运行。