对高速ASIC日益增长的需求使得越来越需要增加电路每个时钟周期的计算吞吐率。可以通过流水线提高ASIC在这方面的性能,但是也会带来系统延迟和面积的增加。
流水线通过在较长的组合逻辑路径中插入寄存器降低了组合逻辑的延迟,从而增加了时钟频率并提高了性能。
时钟频率是数据流入系统后在输出端出现的速率。有许多因素都会影响流水线系统的最大时钟频率。
在实际电路中,由于存在线路上的传播延迟,两个寄存器的时钟输入可能会有一些延迟。这些传播延迟中的微小差别,会发生在复杂数字产品的整个时钟网络上,最后会对整个系统时序产生无法接受的影响,这种现象也称为“时钟偏移”。
在相邻两个寄存器的时钟延迟大于这两个寄存器之间的数据路径延迟时,就会产生负时钟偏移。在这种情况下,先到的时钟会引起竞争条件。即,在数据还未成功锁存时时钟就触发了寄存器。时钟偏移倾向于增加电路所能承受的最大时钟频率。
到达电路中某一点的连续时钟边沿之间间隔的变化称为时钟抖动,时钟抖动会影响时钟的占空比。
流水线使用存储器将时钟周期内关键路径(最大组合延迟的路径)分割开来。这减少了关键路径上各阶段延迟并使电路能以更高频率工作。流水线电路增加了各时钟阶段的计算能力,但是由于使用了存储器单元也增加了负载。
将电路并行化对功耗和面积的影响都很大。一般来说,使用并行电路进行同样的k次操作,比重复使用某一逻辑k次达到同样的效果在功耗和面积方面的开支更大,因为使用了更多的触发器和额外逻辑,导致了更多的连线。
DLX是新兴学院派标准结构的理论32位RISC位处理器。每条DLX指令最多由5个部分组成。
指令获取、指令解码、执行/有效地址周期、存储器访问、写回操作
未插入流水线的实现方式并不能达到最经济和最高的性能,所以很自然会使用流水线设计实现。
操作:
1)从存储器中获取指令(用PC指针)并放入指令寄存器(IR)。
2)IR保存下个时钟周期所需指令。
3)PC值递增4,指向下一个指令地址。
操作:
1)分析IR中的指令并访问寄存器堆以读取寄存器。
2)将通用寄存器的输出读入两个临时寄存器A和B供以后使用。
3)IR的高16位经过符号扩展保存到临时寄存器IMM中供以后使用。
4)由于指令格式是固定的,因此读寄存器和解码可以并行进行。这称为固定域译码。
ALU对上一个时钟周期准备好的操作数进行操作,根据DLX指令的类型执行下面4个功能中的一个:
1)访问存储器:ALU通过加法运算形成有效地址,并将结果放入寄存器ALUoutput。
2)寄存器—寄存器ALU指令:ALU根据操作码对寄存器A和寄存器B中的数值进行操作,把结果放在临时寄存器ALUoutput中。
3)寄存器—立即数ALU指令:ALU根据操作码对寄存器A和寄存器IMM中存放的值进行操作,把结果放入临时寄存器ALUoutput中。
4)分支指令:ALU将NPC和IMM中的带符号立即数相加,计算出分支的目标地址;检查寄存器A(在前一个周期读取)的值来决定是否进行分支。
1)访问存储器:如果指令为载入操作,则把从存储器中返回的数值存入LMD寄存器中;如果指令为存储,则将寄存器B的值写入存储器。
2)分支:如果指令分支,就用ALUoutput寄存器中的分支目标地址替代PC中的值,否则用NPC寄存器中递增的值替代PC中的值。
将结果写回到寄存器堆中,结果可能来自于存储器(LMD)或来自于ALU(ALUoutput)。
下图是无流水线时DLX数据通路上的五个步骤:
这里无法并行执行指令,只有在第一条指令执行完成后才开始执行第二条指令。
这种情况下执行一条指令需要8ns,指令按照顺序一条接一条执行,所以4条指令的集合需要8x4=32ns才能执行完,如下。
对上节的电路加以改造,对5级操作中的每个都加上一个流水线阶段(即在每个阶段加入一组寄存器),修改后的新流水线电路如下所示:
在原始单周期DLX数据通路中,指令无法并行执行,所以只有在前一条指令执行完成后才能处理新的指令。所以假设单周期时钟周期是10ns,依次执行5条指令所用时间为5x10ns=50ns。
而对于多周期数据通路,此时每条指令需要5个时钟周期完成,但每个周期只需2ns。现在如果5条指令像在单周期数据通路中那样连续执行,将花费5x5x2=50ns(即5条指令每条花费5个时钟周期,每个时钟周期为2ns)。
然而,对于流水线模式中同样的数据通路,只需9个时钟周期就可以执行完5条指令,也就是说,5条指令只需9x2ns=18ns就可完成。因此流水线使性能增加了50/18=2.8倍(对单个指令保持延迟值不变)。
需要注意的是,在使用流水线时会引入额外的开销,如时钟偏移和寄存器延迟。这种开销限制了所能达到的加速值。
冒险会干扰流水线并阻止下一条指令在目标时钟周期内的执行。冒险会降低流水线在理想情况下所能带来的速度提升。
冒险分类:
解决以上问题的通用方法是停止流水线直至风险消除,在流水线中插入多个“气泡”(缺口)。
在结构冒险中,硬件无法同时支持所有可能的指令组合重叠执行。结果会导致资源冲突,即多个指令要求访问同一地址。
这里有两条指令在同一个时钟周期内要求访问同一存储器(一个在MEM阶段,一个在IF阶段),因此就会产生存储器冲突。
解决方法是在发生冲突时将流水线停一个时钟周期。这样会产生一个流水线气泡,这样可以在增加一个时钟延迟的情况下执行同样数目的指令,如下:
另一种解决方案是在IF和MEM阶段使用不同的存储器以避免同时访问同一块存储器的冲突,这样虽然解决了冲突冒险,但是消耗了更多的资源。
在数据冒险中,当前指令的执行需要依赖前面指令执行的结果。
第一条指令后的所有指令都使用R1,按下图的方式,ADD之后的指令无法得到正确的执行结果,因为在ADD之后的所有指令的ID阶段都需要ADD指令的结果(在WB阶段输出)。
解决这种冒险的常用方法是数据/寄存器转移。其原理是选择的数据实际上并不在上图所示的ID阶段使用,而是在下图中的ALU(EX)阶段使用。
数据转移规则如下:
存在影响电路得到正确结果的几种不同类型的数据冒险。
因此,数据转移不能解决所有类型的数据冒险。代表性的是直到MEM/WB阶段数据才能使用的情况。
这种冒险通常发生在由于分支语句使程序计数器(PC)发生变化的情况下。如下图所示,在T2周期执行第二条指令时就需要PC的值,但是该值要在第一条指令的MEM操作(周期T4)后才可用。
一种简单的解决方式是在分支语句中用新的PC值重新读取指令。在这种情况下,需要将流水线停止几个周期直到重新获取下一条指令,如下所示:
通过提前预测分支目标或在分支延迟空隙间插入额外指令,可使流水线停止所导致的控制风险最小化。
在指令获取或在数据读写时会访问存储器。
在指令获取时,数据应该由PC寄存器保持稳定。该值应该一直保持到把获取的指令写入IF/ID流水线寄存器的IR域为止。因此,对IF/ID寄存器的IR写操作必须在PC写操作之前。
在数据读写时,访问存储器所用的有效地址在EX阶段由ALU计算出来。在将存储器中的数据存入LMD寄存器或存储器写信号被激活使数据写入存储器之前,该地址不能变化。
实际上,ALU输出值与存储器访问是独立的过程,这就意味着有时EX/MEM ALU的输出值会在数据写入MEM/WB LMD寄存器前更新。需要对这三个事件进行严格的排序以使EX/MEM ALU的输出只在存储器访问完成后才改变。而对于非流水线结构的CPU就不会存在这样的问题,因为指令周期的访问阶段在执行阶段之后,而只有在执行阶段才会修改EX/MEM ALU输出的值。