计算机组成原理:操作数前推,流水线里的接力赛

解决结构冒险和数据冒险有两种方法:

  • 一种是增加资源,通过添加指令缓存和数据缓存,让我们对于指令和数据的访问可以同时进行。这个方法帮助CPU解决了取指令和访问数据之间的资源冲突
  • 另一种方案是直接进行等待。通过插入NOP这样的无效指令,等待之前的指令完成。这样我们就能解决不同指令之间的数据依赖的问题

就好比:

  • 第一种解决方案,好比在软件开发的过程中,发现效率不够,于是研发负责人说:“我们需要双倍的人手和研发资源。”
  • 第二种解决方案,好比在提需求的时候,研发负责人说:“来不及做,你只能等我们需求排期。”

“堆资源”和“等排期”这样的解决方案,并不能真正提高我们的效率,只是避免冲突的无奈之举。

那有没有更高级的解决方案呢?有,操作数前推

NOP 操作和指令对齐

前置知识:

  • MIPS 体系结构下的 R、I、J 三类指令
  • 五级流水线“取指令(IF)- 指令译码(ID)- 指令执行(EX)- 内存访问(MEM)- 数据写回(WB) ”

在这里插入图片描述
在MIPS的体系结构下,不同类型的指令,会在流水线的不同阶段进行不同的操作。

  • LOAD,从内存里读取数据到寄存器的指令,它需要经历的5个完整的流水线。
  • STORE这样从寄存器往内存里写数据的指令,不需要有写回寄存器的操作,也就是没有写回的流水线阶段
  • 对于ADD、SUB这样的加减法治理,所有操作都在寄存器中完成,所以没有实际的内存访问(MEM)操作。

在这里插入图片描述

有的指令没有对应的流水线阶段,但是我们并不能跳过对应的阶段直接执行下一阶段。不然,如果我们先后执行一条LOAD指令和一条ADD指令,就会发生LOAD指令的WB阶段和ADD指令的WB阶段,在同一个时钟周期发生。这样,相当于触发了一个结构冒险事件,产生了资源竞争。
在这里插入图片描述
所以,在实践中,各个指令不需要的阶段,并不会直接跳过,而是会运行一个NOP操作。通过插入一个NOP操作,我们可以使后一条指令的每一个Stage,一定不和前一条指令的通Stage在一个时钟周期执行。这样,就不会发生先后两个指令,在同一时钟周期竞争相同的资源,产生结构冒险了。

在这里插入图片描述

流水线里的接力赛:操作数前推

通过NOP操作进行对齐,我们在流水线里,就不会遇到资源竞争产生的结构冒险问题了。除了可以解决结构冒险之外,这个NOP操作,也就是流水线停顿插入的对应操作。

但是,插入过多的NOP操作,就意味着我们的CPU总是在空转。那么,有没有方法尽量减少一些NOP操作呢?

举个例子:

add $t0, $s2,$s1
add $s2, $s1,$t0
  • 第一条指令,把 s1 和 s2 寄存器里面的数据相加,存入到 t0 这个寄存器里面。
  • 第二条指令,把 s1 和 t0 寄存器里面的数据相加,存入到 s2 这个寄存器里面。

因为后一条的add指令,依赖寄存器t0里的值。而t0里面的值,又来自于前一条指令的计算结果。所以后一条指令,需要等待前一条指令的数据写回阶段完成之后,才能执行。也就是说,遇到了一个数据依赖类型的冒险。于是,我们就不得不通过流水线停顿来解决这个冒险的问题,我们要在第二条指令的译码阶段之后,插入对应的NOP指令,知道前一条指令的数据写回完成之后,才能继续执行。

这样的方案,虽然解决了数据冒险的问题,但是也浪费了两个时钟周期。我们的第2条指令,其实就是多花了2个时钟周期,运行了两次空转的NOP操作。

在这里插入图片描述
不过,其实我们第2条指令的执行,未必要等待第1条指令写回完成,才能进行。如果我们第1条指令的执行结果,能够直接传输给第2条指令的执行阶段,作为输入,那我们的第二条指令,就不用了再从寄存器里面,把数据再单独读出来一次,才执行代码。

我们完全可以在第一条指令的执行阶段完成之后,直接将结果数据传输给下一条指令的ALU。然后下一条指令不需要再插入两个NOP阶段,就可以继续正常走到执行阶段
在这里插入图片描述

这样的解决方案,我们就叫作操作数前推(OperandForwarding),或者操作数旁路(Operand Bypassing)。或者叫做操作数转发。

  • 转发,是这个技术的逻辑含义。也就是在第1条指令的执行结果,直接“转发”给第2条指令的ALU作为输入
  • 旁路,是这个技术的硬件含义。为了能够实现这样的转发,我们在CPU的硬件里面,需要再单独拉一根信号传输的线路出来,使得ALU的计算结果,能够重新回到ALU的输出里来。这样的一条线路,就是我们的“旁路”。它越过(Bypass)了写入寄存器,再从寄存器读出的过程,也为我们节省了 2 个时钟周期。

操作数前推的解决方案不但可以单独使用,还可以和流水线冒泡一起使用。有的时候,虽然我们可以把操作数转发到以一条指令,但是下一条指令仍然需要停顿一个时钟周期。

比如说,我们先去执行一条 LOAD 指令,再去执行 ADD 指令。LOAD 指令在访存阶段才能把数据读取出来,所以下一条指令的执行阶段,需要在访存阶段完成之后,才能进行。

在这里插入图片描述
总的来说,操作数前推的解决方案,比流水线停顿更进了一步。

  • 流水线停顿的方案,有点儿像游泳比赛的接力方式。下一名运动员,需要在前一个运动员游玩了全程之后,触碰到了游泳池壁才能出发。
  • 而操作数前推,就好像短跑接力赛。后一个运动员可以提前抢跑,而前一个运动员会多跑一段主动把交接棒传递给他。

总结

  • 操作数前推,就是通过在硬件层面制造一条旁路,让一条指令的计算结果,可以直接传输给下一个指令,而不需要“指令 1 写回寄存器,指令 2 再读取寄存器”这样多此一举的操作。 这样直接传输带来的好处,后面的指令可以减少,甚至消除原本需要通过流水线停顿,才能解决的数据冒险问题
  • 这个前推的解决方案,不仅可以单独使用,还可以和流水线冒泡结合在一起使用。因为有些时候,操作数前推并不能减少所有“冒泡”,只能去掉其中的一部分。我们仍然需要通过插入一些“气泡”来解决冒险问题
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值