文章目录
流水线的执行效率
假定一条指令流水线由如下 5 5 5个流水段组成。
- 取指令(IF): 从cache或主存取指令。
- 指令译码(ID):产生指令执行所需的控制信号。
- 取操作数(OF) : 读取存储器操作数或寄存器操作数。
- 执行(EX): 对操作数完成指定操作。
- 写回(WB):将操作数写回存储器或寄存器。
理想情况下,每个时钟周期都有一条指令进人流水线,每个时钟周期都有一条指令完成,每条指令的时钟周期数(即CPI)都为 1 1 1。
考虑最复杂的lw
指令的执行情况。假定lw
指令的
5
5
5个阶段所花的操作时间分别如下。①取指
200
p
s
200ps
200ps;②寄存器读:
50
p
s
50ps
50ps;③ALU操作:
100
p
s
100ps
100ps;④存储器读:
200
p
s
200ps
200ps;寄存器写:
50
p
s
50ps
50ps。不考虑控制单元、PC访问、信号传递等延迟,lw
指令的总执行时间为
600
p
s
600ps
600ps。
流水线设计的原则是:指令流水段个数以最复杂指令所用的功能段个数为准;流水段的长度以最复杂的操作所花时间为准。
每条指令的流水段分析
每条指令前两个功能段都一样,它们的功能如下。Ifetch:取指并计算PC+4;Reg/Dec:寄存器取数并译码。后面的功能段随各指令功能的不同而不同。
R型指令功能段划分
R-型指令都涉及在ALU中对Rs和Rt内容进行运算,最终把ALU的运算结果送目的寄存器Rd。像add
和sub
等指令还要判断结果是否溢出,只有不溢出时才写结果到Rd,否则转异常处理程序执行。
根据R-型指令的功能,很容易给出R-型指令的功能段划分。在Ifetch和Reg/Dec两个公共功能段后,其余的是:Exec功能段,用于在ALU中计算;Write功能段,用于将ALU中的计算结果写回寄存器。
I型运算类指令功能段划分
I-型带立即数的运算类指令都涉及对 16 16 16位立即数进行符号扩展或零扩展,然后和Rs的内容进行运算,最终把ALU的运算结果送目的寄存器Rt。显然,I-型运算类指令的功能段划分与R-型指令相同。
lw指令功能段划分
lw
指令的功能为R[Rt]←M[R[Rs]+SignExt(imm16)]
。其功能段的划分如图所示,除公共的两个功能段外,其余的是:1)Exec功能段,用于在ALU中计算地址;2)Mem功能段,用于从存储器中读数据;3)Write功能段,用于将数据写入寄存器。
sw指令功能段划分
sw
指令的功能为M[R[Rs]+SignExt(imm16)]←R[Rt]
,即把寄存器内容写人存储器中,与lw
指令相比,少了一步写寄存器的工作,其功能段划分如图所示。其中,后面两个功能段的功能是, 1)Exec功能段,用于在ALU 中计算地址;2)Mem功能段用于将数据写入存储器中。
beq指令功能段划分
beq
指令的功能为if(R[Rs]=R[Rt]) then PC←PC+4+(SignExt(imm16)×4) else PC←PC+4
。除了前面两个公共功能段外,其后各功能段可以划分为:
1)Exec功能段,用于在ALU中做减法以比较是否相等,同时用一个加法器计算转移地址;2)WrPC功能段,用于在比较相等的情况下将转移目标地址写到PC中。因为写入PC的操作(WrPC)比存储器访问操作(Mem)的时间短,所以,可以将功能段WrPC向功能段Mem靠,即最后的功能段用Mem表示。因此,beq
的功能段划分类似于sw
指令。
j指令功能段划分
j
指令是无条件转移指令,其功能是直接将目标地址送PC中。所以,其功能段的划分很简单,除了两个公共的功能段外,就只有一个功能段WrPC,其操作时间比Exec段时间短。
从以上对各指令功能段的分析可看出,最复杂的是lw
指令,它有
5
5
5个功能段,其他指令都可以通过加入空功能段来向lw
指令靠齐。
在插人“空”段时,应遵循以下两个原则:①每个功能部件每条指令只能用一次(如寄存器写口不能用两次或以上);②每个功能部件必须在相同的阶段被使用(如寄存器写口总是在第 5 5 5阶段被使用)。
因此,R
型指令、I
型运算类指令需在Write之前加一个空的“Mem”段,使得其Write段和lw
指令的Write对齐,都在第
5
5
5段;sw
指令和beq
指令在第
4
4
4个功能段后加一个空的“Write”段;j
指令则在后面添加两个空段“Mem”和“Write”。这样,所有指令都有
5
5
5个功能段。因此,该处理器的指令流水线可以设计成
5
5
5个流水段。
流水线冒险及其处理
结构冒险
结构冒险也称为硬件资源冲突,引起结构冒险的原因在于同一个部件同时被不同指令所用,也就是说它是由硬件资源竞争造成的。
如图所示,若不区分指令存储器和数据存储器而只用一个存储器的话,则在Load
指令取数据的同时,随后的Instr3
正好取指令,此时发生访存冲突。同样,如果不对寄存器堆的写口和读口独立设置的话,Load
和随后的Instr3
也会发生寄存器访问冲突。
解决结构冒险的策略有两个方面:
- 通过功能段划分原则(一个部件每条指令只能使用一次,且只能在特定时钟周期使用),可以避免一部分结构冒险;
- 通过设置多个独立的部件来避免硬件资源冲突。例如,对于寄存器访问冲突,可将寄存器读口和写口独立开来,利用时钟上升沿和下降沿两次触发,使得前半周期使用写口进行寄存写,后半周期使用读口进行寄存器读;对于存储器访存冲突,可把指令存储器IM和数据存储器DM分开,从而使指令和数据的访问各自独立,这样就不会发生结构冒险,如图所示。
数据冒险
数据冒险也称为数据相关。引起数据冒险的原因在千后面指令用到前面指令结果时前面指令结果还没有产生。
上图中,第一条指令的目的寄存器$1
是后面
4
4
4条指令的源寄存器。第一条指令在Wr阶段结束才将结果写到$1
中,而第
2
,
3
,
4
2,3,4
2,3,4条指令分别在第一条指令的Ex、Mem和Wr阶段就要取$1
的内容,显然,如果不采取任何措施,这几条指令取到的是$1
的旧值,只有第
5
5
5条指令xor能取到$1
的新值。从图中看出,所有的数据冒险都是由于前面指令写结果之前后面指令就需要读取而造成的,这种数据冒险称为写后读(Read After Write,RAW)数据冒险。
在非“乱序”执行的基本流水线中,所有数据冒险都属于RAW数据冒险。
对于RAW数据冒险,可以采取以下几个措施。
插入空操作指令
在软件上采取措施,使相关指令延迟执行。最简单的做法是,在编译时预先插人空操作指令nop
。这样做的好处是硬件控制简单,但浪费了指令存储空间和指令执行时间。如图所示,共浪费了3条指令的空间和时间。
插入气泡
在硬件上采取措施,使相关指令延迟执行,通过硬件阻塞(stall)方式阻止后续指令执行。这种硬件阻塞的方式称为“插入气泡(bubble)”,如图所示。
这种方式控制比较复杂,需要修改数据通路。通常要在数据通路中检测哪两条指令发生了相关,以确定是否进行阻塞。阻塞时,可将控制信号清零来阻止结果的写入;也可将指令清零使后续指令执行空操作;或让PC写使能信号清零使PC值不变,从而使当前指令重复执行。这种方式不增加指令条数,但有额外时间开销。
采用转发技术
将数据通路中生成的中间数据直接转发到ALU的输人端。从图可看出,第一条指令在Ex段结束时已经得到$1
的新值,被存放在Ex/Mem流水段寄存器中,因此,可以直接从该流水段寄存器中取出数据送到ALU的输入端,这样,在第二条指令执行时ALU中用的是$1
的新值。同样,第
3
3
3条指令在ALU中用到的$1
也可以直接从Mem/Wr流水段寄存器中取,如图所示。这种技术称为转发或旁路技术。
对于第一条和第四条指令之间的数据相关问题,可以通过将寄存器写口和读口分别控制在前、后半个时钟周期内操作来解决,使前半周期写人
$1
的值在后半周期马上被读出。
采用转发技术解决数据冒险必须在硬件上进行相应的改动,通过在ALU的输人端加多路选择器,使Ex段之后的流水段寄存器的值能返送到ALU输人端。
Load-use数据冒险的检测和处理
如图所示,lw
指令只有在Mem段结束时才能得到DM中的结果,然后送Mem/Wr寄存器,在Wr段前半周期$1
中才能存入新值,但随后的sub
指令在Ex阶段就要取$1
的值,因此,得到的是旧值,而根据转发线路,ALU的输人端要么来自上一条指令在Ex段生成的存放在Ex/Mem寄存器中的值,要么来自上上条指令的执行结果。
由此可知,用转发线路无法解决lw
指令和sub
指令之间的数据相关问题。通常把这种情况称为Load-use数据冒险。对于Load-use数据冒险,最简单的做法是由编译器在load
指令之前插入nop
指令来解决,这样,就无须硬件来处理数据冒险问题。当然,最好的办法是在程序编译时进行优化,通过调整指令顺序以避免出现Load-use现象。
控制冒险
正常情况下,指令在流水线中总是按顺序执行,当遇到改变指令执行顺序的情况时,流水线中指令的正常执行会被阻塞。这种由于发生了指令执行顺序改变而引起的流水线阻塞称为控制冒险。各类转移指令(包括调用、返回指令等)的执行,以及异常和中断的出现都会改变指令执行顺序,因而都可能会引发控制冒险。
转移指令引起的控制冒险
图中,假定beq
指令的地址为
12
12
12,条件满足时其转移目标地址为
1000
1000
1000。
分支指令beq
的转移目标地址计算操作在Ex段,并在Mem段由标志Zero和控制信号Branch来控制,以确定是否将PC的值更新为转移目标地址。
因此,在图所示的例子中,只有当beq
指令执行到第
5
5
5时钟结束才能将转移目标地址
1000
1000
1000送到PC的输入端,在第
6
6
6时钟到来后,取出
1000
1000
1000号单元开始的指令送流水线中执行。此时,紧接在beq
后面的第
16
,
20
,
24
16,20,24
16,20,24单元的指令已在流水线中被执行了一部分。显然,正确的执行流程应该是第
12
12
12单元中的beq
指令执行完后转移到第
1000
1000
1000单元执行,因此,如果不采取相应措施,则指令流水线的执行便发生问题。通常把由于流水线阻塞而带来的延迟执行周期数称为延迟损失时间片
C
C
C。显然,图中的延迟损失时间片
C
=
3
C=3
C=3。
由于指令分支而引起的控制冒险也称为分支冒险。对于分支冒险,可采用和前面解决数据冒险一样的硬件阻塞方式(插人气泡)或软件阻塞方式(插入空操作指令)。
即,假设延迟损失时间片为
C
C
C,则在数据通路中检测到分支指令时,就在分支指令后插入
C
C
C个气泡,或在编译时在分支指令后填入
C
C
C条nop
指令。
插入气泡和插入空操作指令这两种都是消极的方式,效率较低。结合分支预测可以降低由于分支冒险带来的时间损失,分支预测有简单(静态)预测和动态预测两种。此外,还有延迟分支方式也可部分解决分支冒险问题。
- 简单预测
简单预测与指令执行历史无关,因此,它是一种静态预测方式。可以简单预测分支指令的条件总是不满足或总是满足。
对于预测不满足的情况,流水线总是按顺序继续执行分支指令的后续指令,如果在数据通路中检测到实际条件确实不满足时,则预测正确,没有任何时间损失。
如果检测到实际条件满足时,则预测不正确,此时,将分支指令后续不该执行的指令的控制信号清零,实际上只需要将寄存器写信号RegWr和存储器写信号MemWr清零,就能保证不会改变指令执行结果,相当于执行了空操作。这样,如果分支延迟损失时间片为 3 3 3的话,则预测错误时将损失 3 3 3个时钟周期。
- 动态预测
动态预测利用分支指令发生转移的历史情况来进行预测,并根据实际执行情况动态调整预测位。
转移发生历史情况记录在一个表中,这个表有不同的名称,如分支历史记录表、分支预测缓冲、分支目标缓冲等。
每个表项由分支指令的地址作索引,故在分支指令的IF阶段就可取到预测位。因此,完全来得及在分支指令进人ID阶段时去取被预测的指令。
-
一位预测位
采用一位预测位时,总是按上次实际发生的情况来预测下次分支情况,可用1表示最近一次发生转移,0表示未发生转移。
-
两位预测位
用两位组合成4种情况来表示预测和实际转移的状态,4个状态中,有两个状态预测发生转移,有两个状态预测不发生转移。
采用两位预测可避免一位预测时出现的一些问题,使得在连续两次发生不同的分支情况时也可能会预测正确。
- 延迟分支
其主要思想是,采用编译优化来调整指令顺序,把分支指令前与分支指令无关的指令调到分支指令后面执行,以填充延迟损失时间片,不够时用nop
操作填充。分支指令后面被填的指令位置称为分支延迟槽,需要填人的指令条数(即分支延迟槽数)等于延迟损失时间片。
因为延迟分支技术通过编译器重排指令顺序来实现,所以它属于静态调度技术。
异常或中断引起的控制冒险
异常和中断的出现会改变程序的执行流程,使得流水线执行发生阻塞。与分支冒险样,当某条指令执行过程中发现异常或中断时,可能它后面的多条指令已经被取到流水线中正在执行。
例如,ALU运算类指令发现“溢出"时,已经到Exec阶段的结束了,此时,它后面已有两条指令进人了流水线。
通过在数据通路的不同流水段中加人相应的检测逻辑可检测出哪条指令发生了异常。例如,1)“溢出”可在Exec段检出;2)“无效指令”可在ID段检出;3)“除数为O”可在ID段检出;4)“无效指令地址"可在IF段检出;5)“无效数据地址”可在Load/Store
指令的Exec段检出。
检测出异常的那个流水段正在执行的指令就是发生异常的指令。外部中断的检测可以放在第一个流水段IF或最后一个流水段Wr中进行。若放在IF中检测,因为可在取指令前进行,若发现有中断请求发生,则能确保在该时钟周期就开始执行中断服务程序,并让已经在流水线中的指令继续执行完,不需要进行指令冲刷;若放在Wr阶段进行,则需要将刚执行完的指令后面几条指令从流水线中清除掉。
对于五段流水线处理器,任何一个时钟周期都有 5 5 5条活动的指令,因而很可能在一个时钟周期内同时有多条指令发生异常或中断,不同流水段发生不同类型的异常。
例如,在Ex阶段add
指令发生“溢出”的同时,ID阶段的指令发生了“无效指令”,Mem阶段的lw
指令发生了“缺页”,并且又发生了外设I/O中断请求。上述这种情况下,显然应该先响应和处理lw
指令的“缺页”异常。
对于这种同时发生多个异常和中断的情况,最关键的问题是要确定哪条指令的异常应最先被响应和处理。显然,排在前面的指令发生异常的响应优先级高,因此,优先级确定原则是,在同一个时钟周期内的指令序列中,最先执行的指令所产生的异常最先被响应,外部中断请求最后响应,即,对同时在 5 5 5个指令流水段中发生的异常进行排序时,其顺序为Wr>Mem>Ex>ID>IF。
处理器硬件对异常和中断引起的冒险的处理大致的做法如下:当检测到有异常或中断后,首先,清除发生异常的指令以及其后在流水线中的所有指令,然后保存断点,并将异常处理程序的首地址送PC的输人端。
指令清除的方式和上述分支预测错误时指令清除方式类似。
假定正在执行指令序列为lw-add-ori-
,而且lw
指令将在Mem段发生“缺页”,add
指令将在Ex段发生“溢出”,ori
指令将在IF段发生“指令地址越界”。这种情况下,按正确的处理顺序,应该先处理lw
指令的异常。
但是,因为ori
指令处于IF段时,lw
才处于Ex段,add
才处于ID段,因此,此时ori
前面的两条指令都还没有发生异常。如果马上就处理ori
指令的异常,则add
指令和lw
指令的异常就被忽略,从而导致程序被错误执行。
因此,通常的做法是,每个时钟周期内,在多个流水段发生的异常的原因和断点只是被记录到特定的寄存器中,并将发生异常的标记同时记录到流水段寄存器,发生异常的指令继续在流水线中执行,直到执行到最后一个阶段,由最后阶段内的硬件检测本指令是否发生过异常或此时是否有外部中断发生,若有,则清除流水线中后面所有阶段正在执行的指令,然后转到相应的异常处理程序执行。