参考书:《计算机体系结构量化研究方法》 作者:John L. Hennessy
先理解几个基本概念:
流水线:是一种将多条指令重叠执行的实现技术。一条指令的执行需要多个操作,流水线技术充分利用了这些操作之间的并行性。
流水级:不同步骤并行完成不同指令的不同部分。这些步骤中的每一步都称为流水级或流水段
吞吐量:由指令退出流水线的频率决定。
处理器周期:一条指令在流水线中下移一步所需要的时间。一个处理器周期通常为1个时钟周期。(又是为2个,但要少见的多)
流水线的作用:缩短每条指令的平均执行时间。降低了每条指令时钟周期数(CPI)
以MIPS指令集为例,其提供了32个寄存器,通常由以下三类指令:
1)ALU指令:取两个寄存器或者一个寄存器与一个符号扩展立即数进行运算并存储到第三个寄存器中
2)载入和存储指令:这些指令获取一个寄存器源(基址寄存器)和一个立即数字段(16位,偏移量),作为操作数计算有效地址用作存储器地址。
3)分支与跳转:条件转移。通常由两种方式指定分支条件:其一,采用一组条件位(条件码);其二通过两个寄存器之间、寄存器与0之间的对比来设定。MIPS采用后者。
RISC子集中的每条指令都可以在最多5个时钟周期内实现,如下
1)指令提取周期(IF)
将程序计数器PC发送存储器,从存储器提取当前指令。向程序计数器加4(因为每条指令的长度为4个字节),将程序计数器更新到下一个连续计数器。
此周期操作:a.读取PC,b.更新PC
2)指令译码/寄存器提取周期(ID)
此周期操作:a.将指令译码,b.读取相应寄存器,c.判断是否为分支
这几点在下面都会有详细解释
3)执行/有效地址周期(EX)
此周期操作(其中之一):a.计算有效地址(基址寄存器+偏移量)b.在寄存器与寄存器之间进行运算,c.在寄存器与立即数之间进行运算
4)存储器访问(MEM)
操作(执行其中之一):a.将寄存器写入存储器(写入)b.将存储器的内容读取到寄存器中(载入)
5)写回周期(WB)
将程序结果(来自ALU运算或载入指令)写入寄存器堆
下面这种情况没有采用流水线结构,是简单的顺序执行,在执行完一个指令的五个周期后,才能执行下一条指令,如图所示:
这种方法存在着一个问题,比如参与运算的ALU单元平均在五个周期种只被使用了一次,硬件资源造成的很大程度上的浪费。于是我们采用了流水线结构,如下图所示:
流水线结构在每个时钟周期都会启动一条指令,其性能会达到非流水化处理器的5倍(理想情况下)。比如ALU单元,在每一个时钟周期都被用到了,相比于非流水化大大提高了硬件的利用率。IF ID EX MEM WB值得是指令集的5个时钟周期。
可能很难相信流水线是这么地简单,实际上,它的确不是这么简单。
流水线可以看作一系列随时间移动的数据路径。上图给出了数据路径不同部分之间的重叠,时钟周期数5(CC5)表示稳定状态,此状态下,一个时钟周期内,运行着五条指令的不同部分。寄存器左侧虚线表示写入,右侧虚线表示读取。IM表示指令存储器,DM表示数据存储器,CC表示时钟周期。
我们也可以看出来,主要功能单元(如ALU)是在不同周期使用的,因此多条指令的重叠执行不会引发多少冲突(实际上,不可避免会存在些许冲突即冒险问题,在后面会提到及其解决方案)。以下三点可以看出:
1)采用分离的指令存储器及数据存储器,并采用分离缓存的方式避免二者的访问冲突。不过相应开销是:存储器系统必须提供5倍的带宽。
2)在两个阶段都使用了寄存器堆(MIPS中由32个寄存器):一是在ID中进行读取,一是在WB中进行写入。每次时钟周期会执行两次读取和一次写入。我们在时钟的前半周期写寄存器,后半周期读寄存器
3)为了每个时钟周期都会启动一条新指令,必须在IF阶段增加一加法器,一边为下一条指令做准备。此外,我们会在ID期间计算潜在的分支目标。
你会发现流水线最重要的就是:
1. 确保流水线中的指令不会试图在相同时间使用硬件资源。
2. 不同流水级中的指令不会互相干扰
在流水化处理器中,如果要将中间结果从一级传送到另外一级,而源寄存器与目标地址可能并非直接相邻,这时候就是流水线寄存器发挥作用的时候了。例如:要在存储指令中存储的寄存器值是在ID期间进行读取的,但要等到MEM才会真正用到;他在MEM级中通过两个流水线寄存器传送到数据寄存器。于此类似,ALU指令的结果是在EX期间计算的,但要等到WB才会实际存储;
所以有必要对每个寄存器进行命名,称为:IF/ID,ID/EX,EX/MEM,MEM/WB。
流水化作用:提高了CPU指令吞吐量(单位时间内完成的指令数)。但不会缩短单条指令的执行时间。实际上它还会产生额外的开销
流水线开销:流水线寄存器延迟和时钟偏差。
上面介绍的流水线,我们假定指令之间都没有相互依赖关系。可实际上,流水线中的指令可能是存在依赖关系的。
若产生了依赖关系,再按照之前无停顿的顺序执行,就会产生冒险问题。有以下三种冒险:
1)结构冒险:比如两个指令都要访问存储器端口,可存储器只有一个写端口,这咋办?这时候就产生了资源冲突即结构冒险。
2)数据冒险:实际上,指令是存在一些先后顺序的,如果一些指令取决于先前的指令的结果,就可能导致数据冒险。
3)控制冒险:比如C语言中的if else语句,分支指令及其他改变程序计数器的指令实现流水化时可能会导致控制冒险。
如CC4阶段,同时对存储器进行了引用,如果只有一个存储器端口,就会产生冲突。
当指令序列遇到这种冒险时,流水线将会使这些指令中的一个停顿,直到所需单元可用为止。这种停顿会增大CPI的值,使其不再是理想的1。我们把这些停顿称为流水线气泡或气泡。解决流图如下:
载入指令强占了指令提取周期,导致流水线停顿。虽然浪费了流水线中的一个时钟周期,但避免了冲突。
举个栗子,以下指令:
DADD R1,R2,R3 ;R1=R2+R3
DSUB R4,R1,R5 ;R4=R1-R5
AND R6,R1,R7
OR R8,R1,R9
XOR R10,R1,R11
DADD在WB流水级中写入R1的值,但DSUB在其ID中就要读取这个值,这个问题称为数据冒险。因为后面三条指令中使用DADD指令的结果时,由于要等到这些指令读取寄存器之后才会向其中写入,所以会导致冒险。
这一问题,可以采用转发(forwarding)的简单硬件来解决(也称为旁路或短路)。此技术关键是:认识到DSUB要等到DADD实际生成结果之后才会真正用到它。DADD将此结果放在流水线寄存器中,然后转发,转发工作方式如下:
1)来自EX/MEM和MEM/WB流水线寄存器的ALU结果总是被反馈回ALU的输入端
2)如果转发硬件检测到前一个ALU操作对当前ALU操作的源寄存器进行了写入操作,则控制逻辑选择转发结果作为ALU的输入,而不是从寄存器堆中读取。
这种情况不需要停顿,但是并非所有的数据冒险都可以通过转发解决。这就是下面提到的需要停顿的数据冒险
LD R1,0(R2)
DSUB R4,R1,R5
AND R6,R1,R7
OR R8,R1,R9
这个例子与上面的不同在于,上面的指令是ALU运算指令,其在EX阶段就能产生结果,但是载入指令LD就不能在当前周期产生结果,其必须在MEM周期得到结果。因此就没办法仅通过转发技术就消除这一冒险。我们需要增加一种称为流水线互锁以保持正确的执行模式。
流水线互锁会导致流水线停顿,让需要使用某一数据的指令等待,直到源指令生成该数据为止。这种流水线互锁会引入一次停顿或者气泡。如图:
对于MIPS流水线,控制冒险造成的性能损失可能比数据冒险还要大。在执行分支时,修改后的程序计数器的值可能等于也可能不等于当前值加4。介绍一个概念:
选中分支:如果分支将程序计数器改为其目标地址,就代表选中分支,否则就是未选中位置。比如指令i是选中分支,通常会等到ID末尾,完成地址计算和对比之后才会改变程序计数器。
处理分支最简单的方法是:在ID期间(此时对指令进行译码)检测到分支,就对分支之后的指令重新取值。第一个IF周期基本上就是一次停顿,因为它从来不会执行有用工作。如图所示:
如果每个分支产生一个停顿,竟会使性能损失10%-20%,具体取决于分支频率,所以要研究一些用于应对这一损失的技术。
有多种方法可以处理由分支延迟导致的流水线停顿,我们讨论四种简单的编译机制。我也很好奇它为啥会有这么多解决方案???
保留或删除分支之后的所有命令,直到知道目标分支为止。这也是上图的解决方案。这种情况下,开销是固定的,不能通过软件来缩减。
继续提取指令。将每一条分支都看作未选中分支,允许硬件继续执行,就好像分支未被执行一样。如果分支被选中,就需要将已经提取的指令转为空操作,重新开始在目标地址提取指令。但这时候必须特别小心,因为在确切知道分支输入之前,不要改变处理器状态。复杂性在于:必须知道处理器状态可能何时被指令改变,以及如何撤销这种改变。实现过程如图所示:
将所有分支都看作选中分支。只要对分支指令进行了译码并计算了目标地址,我们就假定该分支被选中,开始在目标位置提取和执行。
这种技术在RISC处理器种使用的非常广泛。在延迟分支种,带有一个分支延迟的执行周期为:
分支指令
依序后续指令
选中时的分支目标
依序后续指令位于分支延迟的间隔中。无论分支是否被选中,这一指令都会执行。我们要做的任务就是让这些必须进行的指令有效且可用。首先看一下延迟分支的行为特性。
尽管分支延迟可能长于1个时钟周期,但在实际中,几乎所有具有延迟的分支的处理器都会只有单个指令延迟。为了让这些指令延迟有效并可用,使用了多种优化方式,如下:
延迟分支的局限性在于
a. 对于可排在延迟时隙中的指令有限制
b. 在编译时预测分支是否可能被选中的能力有限。
这种优化也都引入了一种取消或废除分支。在取消分支中,指令包含了预测分支的方向。当分支的行为与预期一致时,分支延迟时隙中的指令就像普通的延迟分支一样执行。不过也有可能把分支预测错误,此时,分支延迟时隙中的指令转为空操作。
这四种方法的不同是:
冻结或冲刷流水线导致对分支后续指令重新取值。此时会有一周期停顿;
预测选中或未选中机制,导致选中分支目标后续指令变为空操作。
延迟分支机制继续执行,不过采用调度,使其执行的指令尽可能有用。如果调度错误会将该分支转为空操作。
首先给出一种简单的非流水化实现,然后介绍流水化实现
每种MIPS指令都可以在最多5个时钟周期中实现,分述如下:
1)指令提取周期(IF)
Mem[PC] ==> IR
PC + 4 ==> NPC
操作:送出PC到IR中,将PC递增4。IR保存将在后续时钟周期中需要的指令。NPC用于保存下一顺序PC
2)指令译码/寄存器提取周期(ID)
Reg[rs] == > A
Reg[rt] == > B
IR的符号扩展立即数字段==> Imm
操作:对该指令进行译码,并访问寄存器堆(rs,rt为源寄存器)放到临时寄存器A和B中,IR的低16位也进行了符号扩展,并存储到临时寄存器Imm中,供下一个周期使用。
3)执行/有效地址周期(EX)
ALU对前一周期准备的操作数进行操作,根据MIPS指令类型执行以下4中功能之一
存储器引用
A + Imm ==> ALUOUTPUT
操作:ALU将操作数相加,得到实际地址,并将结果放在ALUOUTPUT中
寄存器-寄存器ALU指令
A func B ==> ALUOUTPUT
操作:对A,B中的取值执行由功能代码指定的操作,将结果放在ALUOUTPUT中
寄存器-立即数ALU指令
A op Imm ==> ALUOUTPUT
操作:对A,Imm中的取值执行由操作代码指定的操作,将结果放在ALUOUTPUT中
分支
NPC + (Imm << 2)==> ALUOUTPUT
(A == 0) ==> Cond
操作:ALU将NPC加到Imm中的符号扩展立即数,将立即数左移2位,得到一个字偏移量,以计算分支目标。Cond为0是分支的标志。
4)存储器访问(MEM)
对所有的指令更新PC:NPC ==> PC
存储器引用
Mem[ALUOUTPUT] ==> LMD 或
B ==> Mem[ALUOUTPUT]
操作:访问寄存器。如果指令为载入指令,则从存储器返回数据,将其放入LMD(载入存储器数据)寄存器中。其都需要在ALUOUT存放的ALU计算得到的实际地址。
分支
If (cond) ALUOUTPUT ==> PC
操作:如果指令为分支指令,则用寄存器ALUOUTPUT中的分支目标地址代替PC
5)写回周期(WB)
寄存器-寄存器ALU指令
ALUOUTPUT ==> Regs[rd]
寄存器-立即数ALU指令
ALUOUTPUT ==> Regs[rt]
载入指令
LMD ==> Regs[rt]
操作:无论结果来自存储器系统(在LMD中)还是ALU,都将其写到寄存器堆中。
此中,rs,rt为两个源寄存器,rd为目的寄存器。
同时出现一个问题,在写回周期的载入指令中,为什么非要通过LMD才能写回寄存器呢?看下面这个数据流图就明白了:
此中,mux为多工器即数据选择器。
上图为顺序结构,我们几乎不需要什么改变就可以对上图的数据路径实现流水线。如图:
流水线寄存器用于从一个流水级向下一个流水级传送数据和控制。每个流水级上的事件如下:
注意:前两级的操作与当前指令类型无关。由于要等到ID级结束时才会对指令进行译码,所以前两级操作必须与当前指令无关。IF行为取决于EX/MEM中的指令是否为选中分支。如果是,则会在IF结束时将EX/MEM中的分支指令的目标地址写入PC中,如果不是,则写回递增后的PC.
分析一下多工器的作用:
ALU级的两个多工器根据指令类型设定,由ID/EX寄存器的IR字段规定。上面的ALU输入多工器根据该指令是否为分支来设定,下面的多工器根据指令时寄存器-寄存器操作,还是ALU操作还是任意其他类型的操作来设定的。IF级的多工器选择是递增PC的值,还是EX/MEM.ALUOUTPUT的值来写入PC。这个多工器由EX/MEM.Cond字段控制。第四个多工器由WB级的指令是载入指令还是ALU指令来控制。
在MIPS中,分支需要对比两个寄存器的值,在ID周期结束时完成此判断。为了充分利用尽早判断出该分支是否命中懂得优势,都必须尽早计算PC。在ID期间计算分支目标地址需要一个加法器。下图为修改后额流水化数据路径。
增加独立的加法器在ID期间做出分支判断,分支仅需要停顿1个时钟周期
如果存在不可避免地冒险,冒险检测硬件会使流水线停顿。在清除这种相关性之前,不会提取或发射指令。为了弥补这些性能地损失,编译器尝试调度指令来避免冒险;这种方法称为编译器调度或静态调度。
目前为止,讨论的所有技术都是使用循序指令发射,无调度,这意味着如果一条指令在流水线中停顿,将不能处理后续指令。在采用循序发射时,如果两条指令之间存在冒险,即使后面存在一些不相关的、不会停顿的指令,流水线也会停顿。
在流水线中,结构性冒险和数据冒险都是在指令译码ID期间进行检查的:当一条指令可以正确执行时,也是从ID发射出去的。我们必须将发射过程分为两部分:检查结构性冒险以及等待数据冒险的消失。循序对指令进行译码和发射;但是,我们希望指令在其数据操作数可用是立即开始执行。因此流水线是乱序执行的,也就暗示是乱序完成的。为了实现乱序执行,我们必须将ID流水级分为两级。
1)发射:指令译码,检查结构性冒险
2)读取操作数:等到没有数据冒险,随后读取操作数
采用计分卡的动态调度
在动态调度流水线中,所有指令都是循序通过发射级;但是,它们可能在第二级读取操作数级停顿,或者绕过其他指令,然后进行乱序执行状态。记分卡技术再有足够的资源、没有数据依赖时,允许指令乱序执行。这一功能是在CDC 6600记分卡中开发的,因此而得名。
在介绍记分卡之前先介绍一种乱序执行可能会出现的写后读(WAR)数据冒险问题,这在之前循序执行指令时是不会出现的,情况如下:
DIV.D FO,F2,F4
ADD.D F10,F0,F8
SUB.D F8,F8,F14
ADD.D和SUB.D之间存在一种反相关性:意思就是如果乱序执行时先执行了SUB.D就会导致ADD.D数据错误,这就是读后写问题。于此类似,为避免违反输出相关性,也必须检查写后写WAW问题。而记分卡通过停顿反相关中设计的后续指令,避免了两种冒险。
记分卡目标:通过尽早执行指令,保持每时钟周期1条指令的执行速率。
记分卡负责:指令的发射与执行,包括所有的冒险检测任务
要充分利用乱序执行,需要在其EX级中同时有多条指令。这一点可以通过多个功能单元、流水化功能单元或同时利用两者来实现。
CDC6600拥有16个独立的功能单元,包括4个浮点单元5个存储器引用单元和7个整数运算单元。如图:
记分卡的功能时控制指令执行。如上图:共有两个浮点乘法器、一个浮点除法器、一个浮点加法器和一个整数单元。一组总线(两个输入和一个输出)充当一组功能单元。
每条指令进入记分卡,都会构建一条数据相关性记录;并由记分卡判断指令什么时候能够读取它的操作数并开始执行。
在详细介绍记分卡之前,我们需要知道每条指令都需要经历四个执行步骤:
1)发射,如果指令的功能单元空闲,并且不存在冒险时,记分卡向功能单元发射指令,并更新内部数据结构。如果存在结构性冒险或WAW冒险,则指令发射停顿,在清除这些冒险之前,不会再发射其他指令。当发射级停顿时,会导致指令提取与发射之间的缓冲区填满;如果缓冲区是拥有多条指令的队列,则在队列填满后停顿。
2)读取操作数,记分卡见时源操作数的可用性。源操作数可用时,记分卡告诉功能单元继续从寄存器读取操作数,并开始执行。记分卡在这一步动态解决RAW问题,可以发送指令以进行乱序执行
3)执行,功能单元接收到操作数开始执行。结果准备就绪后,通知记分卡已经完成执行
4)写结果,记分卡知道功能单元已经完成执行,则检查WAR冒险,并在必要时停顿正在完成的指令。
一般来说,存在以下情况时,不能允许一条正在执行的指令写入其结果:
a. 在正在执行的指令前边有一条指令还没有读取其操作数(WAR数据冒险)
b. 这些操作数之一与正在执行指令的结果是同一寄存器(WAW数据冒险)
记分卡究竟是怎么执行的呢?看一个例子:
L.D F6,34(R2)
L.D F2,45(R3)
MUL.D F0,F2,F4
SUB.D F8,F6,F2
DIV.D F10,F0,F6
ADD.D F6,F8,F2
记分卡中的信息如下
记分卡共有3个部分:
1)指令状态:指出指令位于4个步骤中的哪一步
2)功能单元状态:指出功能单元FU的状态,共有9个字段
忙:指示该单元是否繁忙。
Op:在此单元中执行的运算,如加减
Fi:目标寄存器
Fj,Fk:源寄存器标号
Qj,Qk:生成源寄存器Fj、Fk的功能单元
Rj,Rk:只是Fj,Fk已准备就绪尚未读取的标记。在读取操作数后将其设置为否
3)寄存器结果状态:如果一条活动指令以该寄存器为目标寄存器,则指出哪个功能单元将写入寄存器。只要没有向该寄存器写入的未完成指令,则将此字段设置为空。
再说明一下每个执行步骤中记分卡都需要做些什么:
记分卡利用ILP(指令级并行),在最大程度上降低因为程序数据相关导致的停顿数目。在消除停顿方面,记分卡受以下几个因素的影响。
1)指令间可用并行数:这一因素决定了能否找到要执行的独立命令
2)记分卡的项数:这一因素决定了流水线为了查找不相关命令可以向前查找多少条指令。这组作为潜在执行对象的指令被称为窗口。记分卡的大小决定了窗口的大小。
3)功能单元的数目和类型:这一因素决定了结构性冒险的重要性,他可能会在使用动态调度是增加。
4)存在反相关和数据相关:它们会导致WAR和WAW停顿。
啊!睡个好觉!