zl程序教程

您现在的位置是:首页 >  其它

当前栏目

【23】冒险和预测(二):流水线里的接力赛

预测 23 流水线
2023-09-11 14:15:48 时间

【计算机组成原理】学习笔记——总目录

引言

上一讲中,
结构冒险的解决方案:简单花钱加硬件电路【堆资源】
数据冒险的解决方案:纯粹等待之前的任务完成【等排期】。
这两种方案都有点儿笨。

更好的方案:计算机组成原理中,一个更加精巧的解决方案,操作数前推

一、NOP 操作和指令对齐

1、回顾

第 5 讲的:MIPS 体系结构下的 R、I、J 三类指令
在这里插入图片描述

第 20 讲的:五级流水线
“取指令(IF)- 指令译码(ID)- 指令执行(EX)- 内存访问(MEM)- 数据写回(WB) ”。
在这里插入图片描述

2、MIPS下,不同类型的指令,会在流水线的不同阶段进行不同的操作

在 MIPS 的体系结构下,不同类型的指令,会在流水线的不同阶段进行不同的操作。

1)MIPS 的 LOAD:从内存里读取数据到寄存器的指令,它需要经历 5 个完整的流水线。
2)STORE :从寄存器往内存里写数据的指令,不需要有写回寄存器的操作,也就是没有数据写回的流水线阶段。
3)像 ADD 和 SUB 这样的加减法指令:所有操作都在寄存器完成,所以没有实际的内存访问(MEM)操作。
在这里插入图片描述

3、产生结构冒险的情况

有些指令没有对应的流水线阶段,但是我们并不能跳过对应的阶段直接执行下一阶段。不然,如果我们先后执行一条 LOAD 指令和一条 ADD 指令,就会发生 LOAD 指令的 WB 阶段和 ADD 指令的 WB 阶段,在同一个时钟周期发生。这样,相当于触发了一个结构冒险事件,产生了资源竞争
在这里插入图片描述

4、如何解决此种结构冒险

所以,在实践当中,各个指令不需要的阶段,并不会直接跳过,而是会运行一次 NOP 操作。通过插入一个 NOP 操作,我们可以使后一条指令的每一个 Stage,一定不和前一条指令的同 Stage 在一个时钟周期执行。这样,就不会发生先后两个指令,在同一时钟周期竞争相同的资源,产生结构冒险了
在这里插入图片描述

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

1、数据依赖冒险的解决方案【操作数前推】

但是,插入过多的 NOP 操作,意味着我们的 CPU 总是在空转,干吃饭不干活。那么,我们有没有什么办法,尽量少插入一些 NOP 操作呢?不要着急,下面我们就以两条先后发生的 ADD 指令作为例子,看看能不能找到一些好的解决方案。

add $t0, $s2,$s1
add $s2, $s1,$t0

这两条指令很简单。
第一条指令,把 s1 和 s2 寄存器里面的数据相加,存入到 t0 这个寄存器里面。
第二条指令,把 s1 和 t0 寄存器里面的数据相加,存入到 s2 这个寄存器里面。

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

这样的方案,虽然解决了数据冒险的问题,但是也浪费了两个时钟周期。我们的第 2 条指令,其实就是多花了 2 个时钟周期,运行了两次空转的 NOP 操作。
在这里插入图片描述不过,其实我们第二条指令的执行,未必要等待第一条指令写回完成,才能进行。如果我们第一条指令的执行结果,能够直接传输给第二条指令的执行阶段,作为输入,那我们的第二条指令,就不用再从寄存器里面,把数据再单独读出来一次,才来执行代码。

我们完全可以在第一条指令的执行阶段完成之后,直接将结果数据传输给到下一条指令的 ALU。然后,下一条指令不需要再插入两个 NOP 阶段,就可以继续正常走到执行阶段。
在这里插入图片描述
这样的解决方案,我们就叫作操作数前推(Operand Forwarding),或者操作数旁路(Operand Bypassing)。其实我觉得,更合适的名字应该叫操作数转发。这里的 Forward,其实就是我们写 Email 时的“转发”(Forward)的意思。不过现有的经典教材的中文翻译一般都叫“前推”,我们也就不去纠正这个说法了,你明白这个意思就好。

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

2、操作数前推和流水线冒泡一起使用 实例

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

比如说,我们先去执行一条 LOAD 指令,再去执行 ADD 指令。LOAD 指令在访存阶段才能把数据读取出来,所以下一条指令的执行阶段,需要在访存阶段完成之后,才能进行。
在这里插入图片描述
总的来说,操作数前推的解决方案,比流水线停顿更进了一步。

流水线停顿的方案,有点儿像游泳比赛的接力方式。下一名运动员,需要在前一个运动员游玩了全程之后,触碰到了游泳池壁才能出发。

操作数前推,就好像短跑接力赛。后一个运动员可以提前抢跑,而前一个运动员会多跑一段主动把交接棒传递给他。

三、总结【个人总结的重点】

  • 更高级的解决数据冒险问题的方案:操作数前推/操作数旁路。

  • 操作数前推,就是通过在硬件层面制造一条旁路,让一条指令的计算结果,可以直接传输给下一条指令,而不再需要“指令 1 写回寄存器,指令 2 再读取寄存器“这样多此一举的操作。

  • 操作数前推带来的好处:这样直接传输带来的好处就是,后面的指令可以减少,甚至消除原本需要通过流水线停顿,才能解决的数据冒险问题。

  • 有些时候,操作数前推可以和流水线冒泡一起使用。因为我们的操作数前推并不能减少所有“冒泡”,只能去掉其中的一部分。我们仍然需要通过插入一些“气泡”来解决冒险问题。

【计算机组成原理】学习笔记——总目录