20. 数据冒险的解决

1. 什么是数据冒险

当前指令需要使用之前指令的运算结果,但是结果还没有写回。

2. 数据冒险举例

比如下图中,第2条add指令需要使用第1条sub指令的结果,第1条sub指令在900ps时才会将运算结果$t0写回寄存器堆,而第2条add指令在500ps时就需要从寄存器堆中读取sub指令的结果了。这里就造成了数据冒险。

数据冒险的例子

3. 数据冒险的解决

3.1 解决方法一:人为的延后add指令-软件延后

如下图所示,软件加入2条nop指令,add指令在第4拍进入流水线,当add指令需要读取寄存器的值时,sub指令刚好已经完成了寄存器的写。

软件加入nop指令解决数据冒险

软件插入nop指令解决数据冒险的方法并不好,因为软件不知道应该插入几条nop指令。上面这样插入2条nop指令可以解决5级流水线的数据冒险问题,那么如果软件移植到8级流水就不可行了。

3.2 解决方法二:人为的延后add指令-硬件延后

对于5级流水,可能插入2个nop指令就可以解决这个数据冒险,但是如果移植程序到一个新型CPU上执行,该新型CPU采用8级流水,这个方法可能就不行了。一般对于软件来说,都要屏蔽硬件的实现细节,因此这里考虑第二种解决数据冒险的方法,硬件插入bubble,如下图所示,

硬件插入bubble解决数据冒险

这里有个问题需要解决,就是硬件如何知道哪里会产生数据冒险和应该插入几个bubble?

流水线每个阶段都在为不同的指令服务,那么检查ID阶段也就是寄存器的读口上的地址,再检查后面各个阶段的硬件操作的寄存器是否和寄存器的读地址相等,就可以判断数据冒险。

这种解决方法不好,因为这样降低了流水线的效率。

3.3 解决方法三:数据前递Forwarding

如下图所示,sub指令实际上在600ps时就已经通过ALU计算出了t0的值,不需要等sub完成写回,就可以将ALU的计算结果传递给下一条指令,即add指令,这就是数据前递。如下图所示,600ps时,sub指令计算结果存放在了EX/MEM流水线寄存器中,将这个结果传递给ALU的输入端完成数据前递。

数据前递解决数据冒险

从硬件实现上来看,如下图所示,直接将ALU经过流水线寄存器的输出接回ALU的输入,完成数据前递。ALU的输入端需要加2选1多路器,控制信号是数据冒险的检测结果。

硬件实现数据前递

再后一级的流水线寄存器的输出也可以做成数据前递,如下图所示,

数据前递

上面紫色线所指示的前递可以解决下面这个数据冒险的例子,第三条指令是一个and指令,当这个and指令ALU需要使用t0的值时,t0刚好完成MEM阶段操作,在MEM/WB流水线寄存器中。

数据前递解决数据冒险

我们注意,再往后的指令,例如instruction 3如果也要使用t0,ID阶段读取寄存器时,sub指令刚好已经完成了WB,就不存在数据冒险了。

因此,对于运算指令,绿色和紫色的前递已经可以解决数据冒险的问题。但是还有一种例外情况。

3.4 load-use冒险

上面对于运算指令的数据冒险已经可以通过前递或者说旁路bypass解决。但是下面这个例子,使用前递就无法解决,

load-use冒险

第4条指令是一个load指令,lw指令使用t0寄存器,这部分已经不存在数据冒险,因为lw指令在ID阶段读取t0寄存器时,sub指令刚好已经完成了WB。

注意这条lw指令在ALU阶段计算出了要访问数据存储器的地址,接着在1400ps时完成了数据存储器的读取,但是lw指令的下一条指令or指令最晚在1200ps就需要t1的数据,这样无论如何都不能将1400ps才可以产生的数据前递至1200ps使用。

解决方法就是那个最简单又最笨的办法,插入bubble。如下图所示,流水线stall一个周期后,再使用紫色的数据前递就可以解决这个load-use冒险。

stall+forwarding解决load-use冒险

推荐阅读更多精彩内容