静态流水线

最常规的指令执行可以视为三个操作:计算pc、取指、执行。如果我们有 n 条指令,不考虑任何的流水线操作,那就需要 3n 个时钟周期 cpu 才能完成 n 条指令的运算。

  1. 第一个时钟:把 npc 的值送入 pc 中。
  2. 第二个时钟:把取出的指令送到 IR 中,并将相关的数据读取至寄存器中。
  3. 第三个时钟:把指令的运算结果送入寄存器或者内存中。

改进1: 我们把第 n+1 条指令的计算 pc 与第 n 条指令的执行并行。

如此说来,我们把原来的 3 个时钟改进为 2 个时钟就可以了。

改进2: 我们在改进1的基础上,能否把第 n+1 条指令的取指与第 n 条指令的执行并行?

在“取指”的过程中,我们还需要获取运算所需的寄存器的值。本条指令的取指过程进行时,上一条指令可能已经完成了写回的过程,所以理论上改进2是可以进行的。不过,如果上一条指令是转移指令,下一条指令的取指取决于转移指令的成功与否,往哪里跳转?所以这两条指令并不能重叠,需要在两者之间插入一条延迟槽指令。经过上述两种改进,整个CPU的所有指令流水线只需要一个时钟周期就可以了。

改进3: 划分指令的执行阶段

但是,考虑到指令的执行过程比较复杂,仅仅把问题考虑的这么简单是不理想的。我们把指令的“整体执行过程”划分为 5 个阶段,按照划分的阶段再细致的考虑流水线的问题。

  1. 取指 IF.
  2. 译码 ID.译码阶段可能会读取寄存器的值、送到运算单元运算。
  3. 执行 EXE.执行的结果可能是我们下一步要访存的地址,或者是准备写回的结果等等。
  4. 访存 MEM.
  5. 写回 WB.

经过上述划分阶段之后,我们还需要在每一个 cpu 执行阶段增加几组寄存器,存储每一个阶段的结果还有其后续流水线所需要的控制信号、目标寄存器号等等信息。

指令相关

指令相关可以分为 3 种,分别是数据相关、结构相关和控制相关。

  1. 数据相关。这种相关比较好判断,常见的有①写后读相关(Read After Write,RAW),就是后面指令要用到前面指令所写的数据,这是最常见的类型,也称为真相关;②写后写相关(Write After. Write, WAW),也称为输出相关,即两条指令写同一个单元,在乱序执行的结构中如果后面的指令先写,前面的指令后写,就会产生错误的结果;③读后写相关( Write After Read, WAR),在乱序执行的结构或者读写指令流水级不一样时,如果后面的写指令执行得快,在前面的读指令读数之前就把目标单元原来的值覆盖掉了,导致读数指令读到了该单元“未来”的值,从而引起错误。
  2. 控制相关。控制相关引起的冲突本质上,是对程序计数器PC的冲突访问引起的。在图中,从第1条指令的 ID 流水级到第2条指令的 IF 流水级的箭头表示控制相关而引起的冲突。即如果第1条指令是转移指令,则第2条指令的取指需要等1拍,等待第1条指令的译码阶段结束才能开始,因为每条指令取指时需要用到 PC 的值,而转移指令会修改 PC。
  3. 结构相关。结构相关引起冲突的原因是两条指令要同时访问流水线中的同一个功能部件。

流水线前递技术

硬件优化。只考虑数据前递给ALU,不考虑前递给存数指令和转移指令的旁路,ALU的每一个输人端都添加了一个3选1逻辑,3个输入分别是原来的ALU输入、下一级流水线输出的结果(即EX流水级ALU的运算结果)以及再下一级流水线输出的结果(即 MEM流水级的结果)。这样,如果后面指令要用到前面指令的运算或访存结果,就可以直接通过运算器前面的多路选择器选择前面指令的运算或访存结果,不用等到前面指令把结果写回到寄存器后再从寄存器读取。

软件实现。软件调度的方法也可以避免由于指令相关引起的冲突。下面通过一个例子来分析指令在 5 级静态流水线中的执行情况,并且介绍编译器的静态指令调度技术如何隔开相关的指令使之避免流水线冲突。例如,要做“A=B+C,D=E-F”的运算,MIPS汇编指令的实现。

在具有前递功能的5级流水线中,由于存在RAW相关,LW和ADD指令之间要空1拍,同样LW和SUB之间也要空1拍,所以执行这8条指令需要10拍。如果把这些指令适当调度一下,在不影响程序正确性的前提下进行重排序,指令执行的效率就会提高。如图所示,把一条LW跟一条SW对调一下,使ADD和SUB相关的LW都隔开了1拍,流水线减少了2拍堵塞。在相同流水线结构的情况下,软件调度技术对于提高指令序列执行的效率是非常重要的。

流水线和例外

在某一条运算,例如“A=B+C”的过程中,可能会出现各种类型的例外(中断),我们需要在操作系统开始处理例外时,硬件要保证例外指令前面的指令都执行完了,后面的指令一条都没动;在流水线中的多条指令同时发生例外的情况下,要保证有序的处理。

解决方案:在5级静态流水线中,为了实现精确例外,可以把指令执行过程中发生的例外先记录下来,到流水线中写回阶段的时候进行处理,这样就保证前面的指令都执行完了,而后面的指令都没有修改机器的状态,而且有两条或多条指令发生例外时,可以处理最前面那条指令的例外。

硬件支持:结合原型CPU的设计,例外信号(EX)及指令的PC要随着流水线前进到写回阶段。每个流水级中间寄存器都增加一个EX项和一个PC项,用来记录发生例外以及例外发生时指令的PC。PC给操作系统进行例外处理时使用,当发生例外的指令处在写回阶段时,CPU要保存该指令的PC值到一个专用的寄存器EPC中,然后把PC置为例外处理程序的人口地址。需要在PC的输入端增加一个2选1逻辑,一个是正常的PC值,一个是例外程序的入口,由例外选通信号来选择。

多功能部件和多拍操作

在CPU中通常存在着不同类型的指令,不同的指令常常由不同的功能部件执行。例如,加减、逻辑运算、转移一般都在定点ALU里执行,定点乘除法通常有专门的部件;浮点的加减﹑取绝对值、取非、定点与浮点的转换操作在浮点ALU中执行,浮点乘除法通常也有专门的部件;CPU的访存指令还需要专门的访存部件。不同的功能部件一般在指令流水线的执行阶段需要不同的执行拍数。在这部分中,我们同样会遇见指令相关的问题。

结构相关。在CPU中有着不同的功能部件处理不同的功能,而且不同指令在不同阶段的执行时间不像5级静态流水线那样简单的划分。处理方法就是stall阻塞。例如,如果访存部件不流水,则会引起多个访存操作的等待,一个Load操作访问cache 不命中时就要访问内存,这可能需要上百拍,后面的访存指令就得等。又如结果写回相关,不同的功能部件延迟不一致,在同一拍写回时会引起寄存器堆写端口冲突。

WAW相关。这里的的例子和上面的结构相关很像,例如前面的指令WB之前需要很多拍的MEM阶段,而后面的指令早就可以WB了,这就发生了WAW相关,解决办法也是stall后面的指令。取数指令需要执行多拍才能写回,而且还可能由于cache失效导致拍数不确定,加法指令执行一拍就可以写回。如果不加以控制,寄存器R1中最后的执行结果就会是取数指令而不是加法指令的结果。为了避免由于WAW相关引起的错误,可以阻塞后面的加法指令,直到前面指令写回后再写回。

RAW相关。读发生在写之后,可以通过数据前递进行部分解决,但是作用十分有限。例如,如果后面的加法指令需要使用前面的访存指令的结果,访存指令需要执行多拍而且不能确定拍数,加法指令就需要等多拍,前递技术只能少等1、2拍。

WAR相关。在静态流水线中,WAR通常不会引起相关,因为读操作发生在ID阶段,在译码阶段指令是有序的,前面的指令没有完成译码,后面的指令就不能前进,因此,后面的指令写寄存器肯定在前面的指令读寄存器之后。但是,在动态调度的指令流水线中,后面的指令可以越过前面的指令读寄存器并执行,这样就会由指令的WAR(Read After Write)相关引起冲突。