流水线(一)-FPGA常见问题社区-FPGA CPLD-ChipDebug

流水线(一)

 

    流水线由一系列的模块组成,这些模块称为流水线级。每一级执行整体任务的一部分,就像一条装配线上的一个工位,执行整体装配任务的一部分,并将这个部分完成的工作传递到下一级。然后又可以开始重复开始做自己分配到的任务。相比于从头到尾用一个单位模块完成一个整体的任务,流水线显然可以在单位时间内完成更多的任务。

1.1普通流水线的基本概念

流水线的吞吐量(单位时间内完成的工作量)取决于耗时最长的流水线级(可以理解为子任务,或装配线的工位)。这样就需要工程师在设计流水线需要均衡的分配任务,以免造成流水线级空闲和资源浪费。想像一下,假如一个工厂的装配流水线完成一个玩具装配需要10个工位,第一个人需要2分钟完成自己的任务,而后面9个人都需要一分钟,很容易理解,后面9个人除去完成自己的任务还需要空闲1分钟等待第一个人完成工作。那么这个流水线的单位时间完成工作量仅取决于第一个人(工作时间最长的)。但如果10个人都需要1分钟完成,那么理论上 ,除去装配第一个玩具后面的人需要等待,每一时刻,他们都是不停工作的。设计流水线也一样,需要合理设计每个流水线级的任务。同时具有可变延迟的流水线级可能会使所有上级停止工作,从而导致数据无法沿着流水线传播。与使用全局信号相比,队列可以使流水线更富于弹性,并能提高流水线对延迟变动的容忍性。

下面总结一下流水线的特点:

(1)将主任务分割为多个子任务

(2)给单个子任务分配独立的专用资源

(3)各子任务按照一定的顺序执行,所有的子任务都完成,主任务就完成

(4)当第一个子处理单元开始处理子任务表示主任务开始,最后一个处理单元执行完毕表示主任务完成。每个子处理单元完成任务后会把结果输入到下一个子单元。

(5)由于每个处理单元之间相互独立,理想状态是每个单元完成任务时间是一致的,否则会产生单元之间相互等待的情况。

(6)主任务分割成的子任务数量称为流水线深度。

(7)理想情况下流水线深度可以无限大,实际上因为设计复杂度,和故障影响度的原因,流水线深度不会设计的特别大,一般为4-8级。

上面所说的,吞吐量¢(这个字母胡乱选的)就是该模块单位时间内可以解决的问题数量(可以执行的任务数量)。比如,一个加法器10ns可以执行完一个加操作,那么它的吞吐量就是100Mo/s(每秒执行100万个加操作)。一个模块的延迟T就是该模块完成一个任务所消耗的时间。例如,加法器执行一次操作,从给定输入到稳定输出,一共消耗10ns,那么它的延迟就是10ns。对一个简单的模块而言,吞吐量和延迟之间互为倒数:¢=1/T。但当我们通过插入流水线的方式会让吞吐量和延迟之间的关系变得更加复杂。

那么流水线和并行什么区别?例如有一个100Mo/s,延迟为10ns的模块,要将其吞吐量增加到4倍,这里假如这个模块已经是最优结构,没有办法通过修改设计模块结构来提速。那么方案一,并行模式。

图片[1]-流水线(一)-FPGA常见问题社区-FPGA CPLD-ChipDebug

模块A-D都是原始模块的副本并且相互独立,四个完整模块并行的执行任务,每个模块执行延迟还是10ns,因为同时执行4个任务所以吞吐量为400Mo/s。简单来说,就像组装玩具车,开了四条组装线,但每条线不是流水线,没有在每条线分配子任务。

方案二,在单个完整的模块来插入流水线,这样将模块A分为四个子模块A1,A2,A3,A4。假定理想状况下,能够做到平均的分配,那么显然每个子模块时间为2.5ns,即TA1=TA2=TA3=TA4=2.5ns

图片[2]-流水线(一)-FPGA常见问题社区-FPGA CPLD-ChipDebug

在2.5ns的时间内,A1模块完成自己的任务a,并将a的结果传递到下一级A2由A2继续加工a,A2又将自己的加工结果传递到下一级。注意,在A1模块完成自己任务a后并把结果传递到A2模块后,又开始自己的任务a。原理和工厂流水线一致。增加流水线的方式,这个系统模块的延迟还是10ns(忽略掉寄存器的延迟),但因为分配子任务,吞吐量已经达到400Mo/s,也就是2.5ns完成一个任务。

从上面的解释可以知道,和并行模式相比,使用流水线可以在仅使用一个完整模块的情况下增加吞吐量,但流水线不是完美的,和并行结构相比,流水线结构会引入更多的寄存器,这会消耗更多资源,以及带来更多的寄存器延迟。举个例子,假如每个寄存器延迟ts为200ps,那么对于并行模块或者单一模块,其总延迟T增加到10.2ns(还是以上面的10ns完成一个任务作为基准),但对于一个4级流水线来讲,整个系统耗费时间达到10.8ns。那么并行4个模块吞吐量¢=1/T=4/10.2=392Mo/s。与此同时,对于一个模块分为4个子模块,则吞吐量¢=1/T=4/10.8=370Mo/s,这显然是巨大劣势。随着流水线的深度增加,两者吞吐量的差距也越来越大。之前说他们相等的前提是忽略寄存器的延迟!因而在实际应用中,大多是把两者结合,使用并级流水线。考虑的基本方式就是,当在单一模块插入流水线导致延迟比较高,对模块影响比较大的时候就不再增加流水线深度,转而实行并行结构继续增加吞吐量,两者结合的方式将会更加合理有效。

1.2流水线示例

一个更加具体的例子,32位的逐位进位加法器,如果想提升运行速度,假如把这个整体的加法器模块分为4个小模块(4个8位),那么每个小模块负责8位的计算。直观的感受一下时序的图,如下。

图片[3]-流水线(一)-FPGA常见问题社区-FPGA CPLD-ChipDebug

每个时钟周期完成一次计算,第一个子任务模块完成0-7位的计算,第二个子任务模块完成8-15位的计算,以此类推。当在第二个时钟周期,第一个子模块又开始了自己0-7位的计算。不知道会不会有人有这样的疑问,既然分成4个模块,为什么不在一个周期同时计算?这就是把并行和流水线搞混了,这四个小模块相互独立,但为了完成整体的32位计算工作不是毫无关系,比如,下一个“工位”需要获得上一个“工位”的进位等信息,看下面的图。

图片[4]-流水线(一)-FPGA常见问题社区-FPGA CPLD-ChipDebug

如上图所示,aR0和bR0是共同输入的两个32位数据用于相加,aR0(7:0)和bR0(7:0)相加输出sR1和位(8位数据之和)并将c8R0的进位信号传输到下一个“工位”(加法器子模块),以此类推,最终获得四个8位的和位,以及一个进位信号。

1.3 流水线停滞

我们之前考虑问题时都是理想状态下,比如每个模块完成自己工作时间是相等的,寄存器延迟是相等的。但实际情况下,我们很难去控制每个模块在绝对相等的时间完成自己的任务,如果上一级完成自己的任务,而下一级还没有完成自己的任务,当上一级把数据传输到下一级,会把下一级数据覆盖,而没有及时完成任务的子模块会丢失掉自己的任务,那么最终的数据也会出现错误。这时则需要在模块之间引入“沟通”的控制流。如图例子,

图片[5]-流水线(一)-FPGA常见问题社区-FPGA CPLD-ChipDebug

 

以S1和S2为例,假如S1准时完成自己的“任务”,但S2没有准时完成自己的任务,如果此时S1发送数据显然会造成错误,所以没有准时完成任务的S2需要把ready信号置为无效,表明自己没有准备好接收数据。同样的,假如S2准时完成自己“任务”但S1没有完成,当时钟沿到来时,S2仍然会接收新的数据,但由于S1没有完成自己的任务,所以会向S2发送一个valid无效信号,表明S2接收的数据属于无效信号,直到valid为有效。

但实际的情况下,有些系统是无法接受经常存在(不经常可能也无法接受)流水线“停滞”。这些“停滞”可能对整个系统带来延迟的影响,进而可能带来性能的不完善,甚至是非常消极的影响。这里就可以通过双重缓冲器来处理这个问题,所谓双重缓冲器就是在每一级的流水线之间放两个寄存器,用作数据的缓冲,这样当某个“工位”出现超时完成,可以将上一级的数据暂存到第二个寄存器,这样就可以让整个流水线动起来。

具体的原理会在下一篇文章介绍,包含代码VHDL的介绍。

请登录后发表评论

    没有回复内容