指令流水线
重点内容,熟练掌握指令执行的多个阶段和流水线的概念,并且能够在有冒险时绘制流水线的时空图。
指令执行的五个阶段
- 取指令(IF - Instruction Fetch): 从内存中取出指令。
- 指令译码(ID - Instruction Decode): 解析指令,确定其类型和操作数。
- 执行(EX - Execute): 执行指令,进行算数运算和逻辑运算等。
- 内存访问(MEM - Memory Access): 访问内存,读取或存储数据。
- 写回(WB - Write Back): 将执行结果写回到寄存器或内存。
指令流水线的基本概念
将指令执行的多个阶段由不同的硬件单独执行,各个阶段可以并行执行。这样,虽然每条指令的执行时间没有缩短,但是CPU的吞吐量得到了提升,也就是说,每单位时间内,CPU可以执行更多的指令。
流水线的冒险和处理
结构冒险
含义
结构冒险是由于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]
四条指令对应的流水线执行如下图所示:
其中 I3
和 I1
之间存在 WAW 数据冒险,I3
和 I2
之间存在 RAW 数据冒险,I4
和 I3
之间存在 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