算数逻辑指令的Data Hazards
在理想的pipeline中,每个周期都会生成一个结果。然而,现实永远是骨感的。
三种hazards (很多人翻译为冒险)会破坏pipeline的流畅运行:
1、Structural hazards ,当不同的stage竞争共享资源时,这也是我们为什么需要区分I-cache和D-cache的原因。
2、Control hazards ,由于指令的流程控制导致的,这也是指令或者说程序的本质。没有分支跳转,计算机就没有灵魂。
3、Data hazards的本质原因就是同一个时刻stage之间的数据依赖性( data dependency)。
假设指令j需要的数据依赖于指令i(指令i的目的操作数作为指令j的源操作数),这种依赖关系被称为 read-after-write(RAW)依赖关系 。
下面是一个RAW的示例:
i: R7 ← R12 + R15
其中有3个RAW依赖关系, 和人世间的纷繁情绪一样,纠缠不清 :
1、指令i+1与指令i有一个RAW依赖关系,因为寄存器R7是指令i+1的输入寄存器,是指令i的输出寄存器。
2、指令i+2与指令i有一个RAW依赖性关系,因为寄存器R7是指令i+2的输入寄存器,是指令i的输出寄存器。
3、指令i+2与指令i+1有一个RAW依赖性关系,因为寄存器R8是指令i+2的输入寄存器,是指令i+1的输出寄存器。
另一种依赖关系,称为 write-after-read(WAR)依赖关系 。寄存器R15是指令i的输入寄存器,是指令i+2的输出寄存器。对于简单的pipeline,这不会有问题。如果我们希望指令i和指令i+2能够乱序执行,那这个问题就必须得到修复,需要用到寄存器重命名的技术了,初衷就是指令i+2先执行时不能影响到指令i,寄存器R15需要保存一个备份。
如果指令i在t时开始,则寄存器R7将在t+4时写入。然而,
对于指令i+1,寄存器R7需要在t+2时从register file读取到ID2EX寄存器;
对于指令i+2,寄存器R7需要在t+3时从register file读取到ID2EX寄存器;
一种可行的解决方案是暂停**(stall )pipeline** ,直到R7被指令i写入。
还有一种解决方案可以避免这pipeline stall ,因为R7的数据在t+2时刻已经产生了,然后在t + 3时刻存储到EX/Mem寄存器中。只要我们能够提供一条路线,将寄存器R7的值连接给指令i+1即可。
同理,需要将t+3时刻的R7值连接到指令i+3的ALU输入,这就是 forward 技术 。
load-store指令的Data Hazards问题
前文我们介绍了算数逻辑指令的Data Hazards,这篇继续介绍load-store指令的Data Hazards问题。
load–store指令的RAW问题,和算数逻辑指令有些区别。
i: R6 ← Mem[R2]
i + 1: R7 ← R6 + R4
在这两个指令之间存在一个RAW依赖关系,因为寄存器R6是指令i的输出寄存器和对指令i+1的输入寄存器。
在这种情况下,因为寄存器R6的值在Mem stage结束后(t+3)才能使用,不可能及时forward的,指令i+1在t+2时刻就需要寄存器R6的值输入到EX stage。这时,别无选择,pipeline只能暂停一个周期,再使用forward技术,形象点的说法就是产生了一个气泡(bubble )。
因此,我们需要修改forwarding control unit,以解决load-store指令的数据依赖问题。
类比于AMBA AHB协议中的hready信号,pipeline stall暂停了当前指令,因为我们希望pipeline中IF/ID寄存器的内容保持不变。判断条件就是在指令i+1的ID阶段检测到前面的指令是一个load ,并且load 的输出寄存器作为当前指令的输入寄存器。
在这种情况下,在pipeline 中插入一个nop操作(不读写内存和寄存器,也不会修改PC值)。
关于forward还有些场景需要考虑:
第一、考虑两个连续指令使用相同输出寄存器的场景,并且这个输出寄存器和下一个指令形成RAW dependency
i: R10 ← R4 + R5
i + 1: R10 ← R4 + R10
i + 2: R8 ← R10 + R7
指令i+2与指令i和指令i+1有RAW依赖关系。这个时候需要指令i + 1 R10值forward给指令i+2,而不是指令i。
第二,内存的一部分被复制到另一部分,如
i: R5 ← Mem[R6]
i + 1: Mem[R8] ← R5
forwarding单元必须识别出这种情况,以便将指令i的寄存器Mem/WB内容forward给指令i+1的store输入,而不是之前的R5寄存器值。
原作者:xpuu