指令流水线

重点内容,熟练掌握指令执行的多个阶段和流水线的概念,并且能够在有冒险时绘制流水线的时空图。

指令执行的五个阶段

  1. 取指令(IF - Instruction Fetch): 从内存中取出指令。
  2. 指令译码(ID - Instruction Decode): 解析指令,确定其类型和操作数。
  3. 执行(EX - Execute): 执行指令,进行算数运算和逻辑运算等。
  4. 内存访问(MEM - Memory Access): 访问内存,读取或存储数据。
  5. 写回(WB - Write Back): 将执行结果写回到寄存器或内存。

指令流水线的基本概念

将指令执行的多个阶段由不同的硬件单独执行,各个阶段可以并行执行。这样,虽然每条指令的执行时间没有缩短,但是CPU的吞吐量得到了提升,也就是说,每单位时间内,CPU可以执行更多的指令。

Instruction
Clock Cycle
1
2
3
4
5
6
7
8
I1
I2
I3
I4
IF
ID
EX
M
WB
IF
ID
EX
M
WB
IF
ID
EX
M
WB
IF
ID
EX
M
WB

流水线的冒险和处理

结构冒险

含义

结构冒险是由于CPU的硬件资源有限而引起的。当两条或多条指令需要使用同一硬件资源时,就会发生结构冒险。

处理方法

  • 资源重复:增加硬件资源的数量,例如增加ALU或内存的端口数量。
  • 流水线阶段调度:通过调整流水线的执行顺序或延迟某些指令的执行来避免冒险。

例子

// 如果CPU只有一个内存端口,那么I1和I2不能在同一个周期访问内存,这就产生了结构冒险。
I1: LOAD R1, 0(R2)    // 将内存地址R2+0的值加载到寄存器R1
I2: STORE R3, 4(R4)   // 将寄存器R3的值存储到内存地址R4+4

数据冒险

含义

数据冒险是由指令之间的依赖性引起的。一条指令可能需要使用另一条指令的结果,如果这些指令过早地进入流水线,它们可能会尝试在数据准备好之前使用数据。

数据冒险可以分为三类:

  • 写后读(RAW, Read After Write):在一条指令尝试读取一个数据项的值时,而这个数据项的值还没有被前一条指令写入。
  • 读后写(WAR, Write After Read):一条指令尝试写入一个数据项的值时,而这个数据项的值还没有被后一条指令读取。
  • 写后写(WAW, Write After Write):两条指令尝试写入同一个数据项的情况,如果这两条指令的执行顺序不当,可能会导致不一致的结果。

处理方法

  • 流水线停顿(Pipeline Stall):暂停流水线直到数据准备好。
  • 重新排序指令(Instruction Reordering):编译器在编译时对指令进行重新排序,以减少数据冒险。
  • 数据前推(Data Forwarding):设置相关专用通路,直接将前一条指令的结果传递给需要它的下一条指令,不等结果写回寄存器。

例子

下述代码给出了数据冒险的三种情况:

// RAW
I1: ADD R1, R2, R3    // R1 = R2 + R3
I2: SUB R4, R1, R5    // R4 = R1 - R5

// WAR
I1: LOAD R1, 0(R2)    // R1 = Memory[R2+0]
I2: STORE R3, 0(R2)   // Memory[R2+0] = R3

// WAW
I1: MUL R1, R2, R3    // R1 = R2 * R3
I2: ADD R1, R4, R5    // R1 = R4 + R5

高级语言一条赋值语句被汇编微如下四条指令:

I1    LOAD  R1, [a]
I2    LOAD  R2, [b]
I3     ADD  R1, R2
I4   STORE  R1, [x]

四条指令对应的流水线执行如下图所示:

Instruction
Clock Cycle
1
2
3
4
5
6
7
8
I1
I2
I3
I4
IF
ID
EX
M
WB
IF
ID
EX
M
WB
IF
9
10
11
12
13
ID
EX
M
WB
IF
ID
EX
M
WB
14

其中 I3I1 之间存在 WAW 数据冒险,I3I2 之间存在 RAW 数据冒险,I4I3 之间存在 WAR数据冒险,所以 I3 必须等待 I1, I2 执行完才能进行后续操作, I4 必须等待 I3 执行完才能进行后续操作。

控制冒险

含义

控制冒险是由分支和跳转指令引起的。因为CPU需要在执行分支和跳转指令后,才能知道下一条要执行的指令在哪里,这导致了流水线的暂停或者无效的指令进入流水线。

处理方法

  • 预测不跳转(Predict Not Taken):预测每个分支都不会被采取,当分支被采取时,取消流水线中的指令并从正确的位置重新开始。
  • 预测跳转(Predict Taken):预测每个分支都会被采取,对预测错误的情况进行修正。
  • 延迟分支(Delayed Branch):编译器或处理器对代码进行优化,将分支指令后的一些不依赖于分支结果的指令先执行,从而减少因分支预测错误造成的开销。

实例

// 直到I1被执行,我们都不知道接下来是执行I2还是跳转到I3
I1: BEQ R1, R2, Label  // 如果R1等于R2,则跳转到Label
I2: ADD R3, R4, R5     // R3 = R4 + R5
I3: Label: MUL R6, R7, R8  // R6 = R7 * R8