9.3 指令相关和流水线冲突
前面设计的流水线处理器在执行图9.7中所示的简单指令序列时可以很顺畅,每个时钟周期都能执行完一条指令。但是程序中的指令序列并不总是这么简单,通常会存在指令间的相关,这就有可能导致流水线处理器执行出错。举例来说,对于“add.w $r2,$r1,$r1;add.w $r3,$r2,$r2”这个指令序列,第1条指令将结果写入r2寄存器,第2条指令再用r2寄存器的值进行计算。在前面设计的5级静态流水线处理器中,第1条指令在第5级写回阶段才把结果写回到寄存器,但是第2条指令在第2级译码阶段(此时第1条指令尚在第3级执行阶段)就已经在读寄存器的值了,所以第2条指令读取的是r2寄存器的旧值,从而造成了运算结果错误。因此本节将重点探讨如何在流水线处理器结构设计中处理好指令相关,从而保证程序的正确执行。
指令间的相关可以分为3类:数据相关、控制相关和结构相关。在程序中,如果两条指令访问同一个寄存器或内存单元,而且这两条指令中至少有1条是写该寄存器或内存单元的指令,那么这两条指令之间就存在数据相关。上面举的例子就是一种数据相关。如果两条指令中一条是转移指令且另一条指令是否被执行取决于该转移指令的执行结果,则这两条指令之间存在控制相关。如果两条指令使用同一份硬件资源,则这两条指令之间存在结构相关。
在程序中,指令间的相关是普遍存在的。这些相关给指令增加了一个序关系,要求指令的执行必须满足这些序关系,否则执行的结果就会出错。为了保证程序的正确执行,处理器结构设计必须满足这些序关系。指令间的序关系有些是很容易满足的,例如两条相关的指令之间隔得足够远,后面的指令开始取指执行时前面的指令早就执行完了,那么处理器结构设计就不用做特殊处理。但是如果两条相关的指令挨得很近,尤其是都在指令流水线的不同阶段时,就需要用结构设计来保证这两条指令在执行时满足它们的相关关系。
相关的指令在一个具体的处理器结构中执行时可能会导致冲突(hazard)。例如本节开头所举例子中,数据相关指令序列在5级静态流水线处理器中执行时碰到的读数时机早于写数的情况就是一个冲突。下面将具体分析5级静态流水线处理器中存在的冲突及其解决办法。
9.3.1 数据相关引发的冲突及解决办法
数据相关根据冲突访问读和写的次序可以分为3种。第1种是写后读(Read After Write,简称RAW)相关,即后面指令要用到前面指令所写的数据,也称为真相关。第2种是写后写(Write After Write,简称WAW)相关,即两条指令写同一个单元,也称为输出相关。第3种是读后写(Write After Read,简称WAR)相关,即后面的指令覆盖前面指令所读的单元,也称为反相关。在9.2节所介绍的5级简单流水线中,只有RAW相关会引起流水线冲突,WAR相关和WAW相关不会引起流水线冲突。但是在9.4节中将要介绍的乱序执行流水线中,WAR相关和WAW相关也有可能引起流水线冲突。
下面重点分析RAW相关所引起的流水线冲突并讨论其解决方法。对于如下指令序列:
add.w $r2,$r1,$r1 add.w $r3, $r2, $r2 ld.w $r4, $r3, 0 add.w $r5, $r4, $r4
其中第1、2条指令间,第2、3条指令间,第3、4条指令间存在RAW相关。这3条指令在9.2节所介绍的5级简单流水线处理器中执行的流水线时空图如图9.9所示。
图9.9中从第1条指令的写回阶段指向第2条指令的译码阶段的箭头以及从第2条指令的写回阶段指向第3条指令的译码阶段的箭头都表示RAW相关会引起冲突。这是因为如果第2条指令要使用第1条指令写回到寄存器的结果,就必须保证第2条指令读取寄存器的时候第1条指令的结果已经写回到寄存器中了,而现有的5级流水线结构如果不加控制,第2条指令就会在第1条指令写回寄存器之前读取寄存器,从而引发数据错误。为了保证执行的正确,一种最直接的解决方式是让第2条指令在译码阶段等待(阻塞)3拍,直到第1条指令将结果写入寄存器后才能读取寄存器,进入后续的执行阶段。同样的方式亦适用于第2、3条指令之间和第3、4条指令之间。采用阻塞解决数据相关的流水线时空图如图9.10所示。
图9.9 RAW数据相关的流水线时空图
图9.10 用阻塞解决数据相关的流水线时空图
阻塞功能在处理器流水线中的具体电路实现是:将被阻塞流水级所在的寄存器保持原值不变,同时向被阻塞流水级的下一级流水级输入指令无效信号,用流水线空泡(Bubble)填充。对于图9.10所示的流水线阻塞,从每个处理器部件的角度所看到的时空图如图9.11所示。
图9.11 有阻塞的处理器部件时空图
流水线前递技术
采用阻塞的方式虽然能够解决RAW相关所引发的流水线冲突,但是阻塞势必引起流水线执行效率的降低,为此需要更为高效的解决方式。继续分析前面所举的例子,可以发现第2条指令位于译码阶段的时候,虽然它所需要的第1条指令的结果还不在寄存器中,但是这个值已经在流水线的执行阶段计算出来了,那么何必非要等着这个值沿着流水线一级一级送下去写入寄存器后再从寄存器中读出呢?直接把这个值取过来用不也是可行的吗?顺着这个思路就产生了流水线前递(Forwarding)技术。其具体实现是在流水线中读取指令源操作数的地方通过多路选择器直接把前面指令的运算结果作为后面指令的输入。考虑到加法指令在执行级就完成了运算,因此能够设计一条通路,将这个结果前递至读寄存器的地方,即有一条从执行级到译码级的前递通路。除此之外,还可以依次添加从访存级、写回级到译码级的前递通路。新的流水线时空图如图9.12所示。
图9.12 加入前递的数据相关时空图
可以看出,加入前递技术之后,执行这4条指令的性能有大幅提高。
通过前面对于指令相关的分析,我们需要在处理器中加入阻塞流水线的控制逻辑以及前递通路。演进后的处理器结构如图9.13所示。为了表达清晰,图中省略了时钟信号到每组触发器的连接线。
图9.13 处理指令相关的流水线结构图
图9.13中虚线框中是新加入的逻辑。为了解决数据相关,加入了寄存器相关判断逻辑,收集当前流水线中处于执行、访存及写回级的最多3条指令的目的寄存器信息,与译码级的源寄存器比较,并根据比较结果决定是否阻塞译码级R1;为了解决控制相关,加入了译码级和执行级能够修改PC级有效位的通路;为了解决结构相关,加入了译码级到PC级的阻塞控制逻辑;为了支持前递,加入了从执行级、访存级到译码级的数据通路,并使用寄存器相关判断逻辑来控制如何前递。可以看出,大多数机制都加在了前两级流水线上。
9.3.2 控制相关引发的冲突及解决办法
控制相关引发的冲突本质上是对程序计数器PC的冲突访问引起的。图9.14中的箭头即表示控制相关所引发的冲突。
图9.14 控制相关示意图
按照图9.6给出的处理器设计,执行阶段R2触发器所存储的值经过计算之后才能给出转移指令的正确目标并在下一个时钟上升沿更新PC,但是图9.13中转移指令尚未执行结束时,PC已经更新完毕并取指了,从而可能导致取回了错误的指令。为了解决这个问题,可以通过在取指阶段引入2拍的流水线阻塞来解决,如图9.15所示。
在单发射5级静态流水线中,如果增加专用的运算资源将转移指令条件判断和计算下一条指令PC的处理调整到译码阶段,那么转移指令后面的指令只需要在取指阶段等1拍,调整后前述代码序列的执行流水线的时空图如图9.16所示。采用这种解决控制相关的方式,继续改进流水线处理器结构,得到如图9.17所示的结构设计。
图9.15 解决控制相关的流水线时空图
图9.16 优化控制相关处理后的流水线时空图
图9.17 改进后的解决控制相关的流水线结构图
为更进一步减少由控制相关引起的阻塞,可以采用转移指令的延迟槽技术,在定义指令系统的时候就明确转移指令延迟槽指令的执行不依赖于转移指令的结果,这样转移指令后面的指令在取指阶段1拍也不用等。总之,在单发射5级静态流水线处理器中,通过在译码阶段对转移指令进行处理和利用转移指令延迟槽技术,就可以避免控制相关引起的流水线阻塞。但是这两项技术并不一定适用于其他结构,后面9.5.3节讨论转移预测技术时将做进一步分析。
9.3.3 结构相关引发的冲突及解决办法
结构相关引起冲突的原因是两条指令要同时访问流水线中的同一个功能部件。回顾前面图9.10中所示的指令序列执行情况,由于流水线中只有一个译码部件,所以第3条指令因为结构相关在第7个时钟周期之前不能进入译码阶段,否则就将覆盖第2条指令的信息,导致第2条指令无法正确执行。同样,可以看到不存在任何数据相关的第4条指令,由于存在结构相关也被多次阻塞,甚至被堵得还无法进入取指阶段。