标签:组成 存在 时钟周期 进入 超标量 一段 例子 实现 还需
在程序当中,我们经常会对同一个变量进行反复的使用和修改。这样对于流水线处理器来说,就会经常出现数据冒险的情况,我们必须很好的应对和解决。在这一节,我们就来看一看有哪一些不同的解决方法。
我们先来看这个数据冒险的例子。产生这个数据冒险,是因为第二条加法指令会用到第一条减法指令的运算结果。但是在流水线当中,这条加法指令在读取t0寄存器的时候,它前一条减法指令还没有把运算结果写到t0寄存器当中去。所以,这里就存在一个数据冒险。要解决这个数据冒险,最简单的方法,实际上是在软件层面进行解决。
假设我们这个处理器的流水线并不能解决这样的数据冒险。那其实,我们只要通过编程的手段,人为的将这条加法指令推后执行,让他读取寄存器堆的时间推后到减法指令写寄存器堆的时间之后。那这应该怎么做呢?
我们有一条指令叫做nop,它的作用是什么也不干。我们就在这个减法指令和加法指令之间插入两个nop指令。这两个nop指令只是简单的通过流水线,并占用了相对应的时间。那这样刚才的这个数据冒险至少是不存在了。而因为这两个nop指令的作用,加法指令推后了两个周期才进入流水线,那么当这条加法指令需要读寄存器堆的时候,前面减法指令已经完成了对寄存器堆的写,那加法指令就可以从寄存器堆当中读到正确t0的值,从而完成正确的加法运算。所以,解决这个数据冒险最简单的方法就是插入nop指令,但是这个方法也有很大的问题。
首先,到底应该插入几个nop指令,这是和流水线的结构相关的。如果我们这一段程序放在一个5级流水线上是正常运行的。那过几天,又出了一个更新的处理器,它的流水线是8级的,那这个程序放上去,可能运行就会发生错误。因为流水线变深之后,解决数据冒险需要等待的周期数可能会变多。所以,插入nop的这个方法可行,但是并不好。在一般情况下,我们还希望软件屏蔽硬件的这些实现细节。那既然加了两个nop指令能够解决问题,那么就可以尝试在硬件上完成相同的工作。
刚才通过插nop的方法,其实已经给我们提供了借鉴。我们只要发现存在这样的数据冒险,我们就在硬件的流水线上让各个控制信号都变成执行nop指令一样的值。那在这两个周期,就会产生流水线停顿的效果。而这些和nop指令效果一样的控制信号,它们所产生的状态就成为一个空泡。这个空泡随着时钟周期一级一级往后面传,从效果上来看,和nop指令在流水线当中一级一级的执行是一样的。只是区别在于,这样的信号是由硬件来产生的。
那现在又有了一个新的问题,如果刚才是在软件中插入了nop指令,那对于这个流水线来说,它是严格的按照取回一条指令进行执行,这样的方式来运转的。那现在需要在硬件上自动的插入空泡,就需要一个方式来检测是否出现了数据冒险。当然这也不难,如果我们不是看这一段程序代码,而是看处理器当中的这五个部件,那我们怎么来判断存在数据冒险呢?
所谓数据冒险,就是当前有一条指令要读寄存器,而它之前的指令要写寄存器,但又没有完成。所以,我们只用检查,在译码这个阶段,需要读的寄存器的编号,这个通过链接在寄存器读口的信号就可以得到。然后我们再检查后面各个阶段,其实在每一级,都有些信号能够表明这条指令是否要写某个寄存器,以及要写哪个寄存器。因此,我们只需要检查后面每一个阶段所要写的寄存器的编号,和当前译码阶段,所要读读寄存器的编号是否有相同。如果存在相同,那就是有数据冒险。只要出现来数据冒险,我们就在流水线中插入空泡。这样我们就能通过硬件来解决数据冒险的问题。但是,在实际的编程当中,这种先写了一个寄存器,然后很快使用的状况是经常出现的。如果说每次出现,我们都要让流水线停顿的话,对性能的影响就太大了。所以,我们不能只追求做对,还要要求做好。我们还是希望流水线不要停顿。
那这个就是最初我们分析的样子。减法指令在800ps之后才开始写寄存器,而加法指令最晚在500ps的时候就要去读寄存器。我们无法逆转这个时间,所以我们肯定不能把800ps才有的数送到500ps的这个时间去。但是我们可以换一个角度想一想。这条减法指令的运行结果真的是在800ps这个时候才有的吗?实上减法运算是在执行阶段(EX)由ALU这个部件完成的。所以,最晚在600ps时候,要写到t0寄存器当中的数就已经运算完成了。所以,从时间角度来看,在600ps之后,我们就可以得到t0寄存器的最新的值。而对于这条加法指令,它真的需要使用t0寄存器的值是在它的执行阶段,也就是ALU的部件需要用t0的值作为其中的一个输入,那这个阶段是在600ps之后才开始的,我们完全可以将减法运算的结果交给这个加法运算作为输入。这种方法就叫做数据前递,也就是上一条指令将自己的运算结果往前传递到下一条指令去。
那我们刚才已经分析过,在600ps的时候,ALU的输出结果已经是t0的值了,那在600ps这个时钟上升沿过去之后,t0的这个值会被保存到执行(EX)和访存(MEM)之间的这个流水线寄存器当中去。我们如果把它传递给ALU的输入,就可以正确的完成后面这条加法运算了。
那既然从时间上是可行的,我们就可以来看一看硬件上怎么来修改。
这条减法指令在执行完运算以后,运算结果已经保存到了这个寄存器当中(1处)。 那现在,这条减法指令进入到访存阶段,t0的值将会通过这个阶段传到下一级流水线寄存器(2处)。而与此同时,加法指令正在执行阶段,它需要将t0寄存器的值送到ALU的一个输入端(3处),那显然,ALU的上一个阶段从寄存器堆当中读到的值(4处),肯定不是最新的。现在这个最新的值在访存阶段的连线上。所以,我们从硬件连线上可以把这个信号引回来,从新引到ALU的输入端(5处)。
当然,这里(3处)我们还需要增加一个多选器,而且我们刚才也讲过,如何去判断在流水线当中出现了数据冒险。那我们就可以用这样的判断结果作为这个多选器的选择信号,在出现数据冒险的时候,我们选择这个前递的信号。当然,这条加法指令也有可能在第二个原操作数(s4)上使用了t0寄存器。所以,这个前递的信号还应该传送到ALU的另一个输入端,当然在这里(ALU的另一个输入端,即3处下面)也需要加上多选器来进行选择。
那这样的方式就被成为前递,它还有个名称叫作旁路。从根本上来说,前递和旁路指的都是这件事情。只不过是观察和描述的角度不同而已。前递是从指令执行顺序的角度来描述的,而旁路则是从电路的结构角度来描述。本来前一条指令应该将运行的结果写入到寄存器堆(6处),然后再交给后一条指令使用。而我们现在搭建了一条新的通路,相当于绕过了寄存器堆,直接进行了数据的传递。所以,从硬件实现的角度来看,这是一个旁路。
那这就是前递和旁路的关系。那我们进一步来看,其实不仅仅在这个点(7处)可以建立旁路,我们在下一个流水级也可以建立旁路(8处)。
那这条旁路在什么情况下会用上呢?
我们还是结合一个例子来看。这个例子前两条指令和刚才的那个例子是一样的,在此基础上我们又写出了第三条指令。这是一个与操作,那么它其中的一个源操作数也是t0。那我们结合实践来看,对于这条与操作指令,它真的要开始运算的时候,是在800ps之后,在这个时候,前面这条减法指令已经完成了访存阶段(MEM)。所以,t0寄存器的最新值现在是放在访存阶段和写回阶段之间的流水线寄存器当中的。那我们就需要用到刚才的结构图当中紫色的旁路的线,用来将t0的内容传递到ALU的输入端,从而让这条与运算指令能够及时的运行。
那如果再往后一条指令又用到了t0会怎么样呢?那么这个标着3的指令在800ps之后的这个时钟周期正好进入了译码阶段(ID),它会在这个周期的后半部分读取寄存器。那么在这个时候,减法指令已经将t0的值写入到了寄存器堆中。所以,对于这个3号指令,如果它用到了t0这个寄存器,它就可以按照正常的操作,从寄存器堆当中读出t0寄存器的值,而不需要使用前递的技术。 所以,对于这样运算指令,我们建立的这两组旁路的通路,就已经可以解决数据冒险了。但是还是有一种例外的情况,我们通过一个新的例子来看。
在这个例子当中,前三条指令还是和刚才一样,第四条是一个Load指令,它也会用到t0寄存器,但是我们刚才已经分析过了,这个时候并不存在数据冒险。而这条Load指令是要把存储器当中的一个数取出来,存放到t1寄存器当中去。而它之后,一条或运算指令会使用t0寄存器的值,那这种情况就是一条Load指令之后跟了一条指令,会使用Load指令的目的寄存器。那在这种情况下,也会发生数据冒险。它有个专门的名称,叫作load-use冒险。
那么这种冒险是否也可以用前递的技术来解决呢?实际上是做不到的,那我们来分析一下为什么做不到。对于这一条Load指令,我们来看要保存到t1寄存器的值,究竟是什么时候才得到的?对于刚才的运算指令,需要写回寄存器的值,是在执行阶段,也就是通过ALU运算而得。但是对于Load指令,用ALU是计算要访存的地址,而要写回寄存器堆的数,是在访存阶段的结束才会得到。所以,是在1400ps这个地方,我们才会得到t1寄存器的值。而对于下面这一条或运算指令,我们最晚也得在1200ps这个地方,得到t1这个寄存器的值,从而让ALU可以进行正确的运算。因此,这就要求我们将1400ps这个地方得到的数,传递到之前1200ps这个时刻。时光倒流的事情我们是做不到的。所以,我们只能让信号沿着时间轴向前传递,而绝不可能向后传递,因此,无论我们怎么修改电路,也无法构造出一条前递的通路。那我们应该怎么来解决这个load-use的这个冒险呢?其实说难很难,说简单也就很简单。
还是用我们那个万能的方法。既然我们不能返回到更早的时间,那我们只能让这条或运算指令多等一个周期,这样它就可以在1400ps之后才需要这个t1寄存器的值,而此时Load指令已经完成了从数据存储器当中取出数的操作。这就可以通过刚才我们已经建立的第二组旁路通道,也就是用紫色的连线表达的这个旁路通道,将t1寄存器的内容传送到ALU的输入端口。那当然,既然我们要让或运算指令延后一个周期,我们就必须在流水线中插入空泡,让流水线产生一次停顿。所以,对于这种冒险,我们需要用流水线停顿再加上数据前递的方式来解决。
那这个解决方案没有让流水线获得最高的指令吞吐率,这当然是一个遗憾,但是保证指令执行正确才是我们的首要目标。所以,我们也只能接受这样的方案了。
现在,对于一个基本的流水线结构,我们已经能够处理数据冒险了。但是,如果继续增加流水线的深度,或者扩展成超标量流水线,又会出现新的数据冒险的情况。当然,与之对应的又有很多精巧的解决方案。如果你对此感兴趣,还可以进一步的深入学习。
标签:组成 存在 时钟周期 进入 超标量 一段 例子 实现 还需
原文地址:https://www.cnblogs.com/doctorx/p/9737442.html