赞
踩
如下图所示的浮点加法流水线
它的时空图
流水线技术的特点:
由于反馈回路的存在,当一个任务在流水线中流过时,可能要多次经过某些段,这样就不能每个时钟周期向流水线送入一个新任务。
对于多功能流水线来说,由于不同功能的任务可以相互穿插在一起流入流水线,其调度复杂得多。下面仅以双功能(设为功能A和B)非线性流水线为例,说明多功能非线性流水线的最优调度方法。双功能非线性流水线的调度方法与单功能类似,只是其状态转移图中结点状态的表示不同,是由两个冲突向量构成的冲突矩阵,这两个冲突向量分别对应于下一个任务的功能是A类和B类的情况。而且,其初始结点有两个,分别对应于第一个任务是A类和B类的情况。假设当第一个任务是A类时,其冲突矩阵为
M
A
(
0
)
M_A^{(0)}
MA(0);当第一个任务是B类时,其冲突矩阵为
M
B
(
0
)
M_B^{(0)}
MB(0),则有:
M
A
(
0
)
=
[
C
A
A
C
A
B
]
,
M
B
(
0
)
=
[
C
B
A
C
B
B
]
M_A^{(0)}=
其中,
C
p
q
(
p
,
q
∈
{
A
,
B
}
)
C_{pq}(p,q\in\{A,B\})
Cpq(p,q∈{A,B})表示的是:在一个
p
p
p类任务流入流水线后,对后续
q
q
q类任务的冲突向量,它们可以由预约表求得。
对于后续状态的冲突矩阵由下式求得:
S
H
R
(
i
)
(
M
k
)
∨
M
r
(
0
)
SHR^{(i)}(M_k)\vee M_r^{(0)}
SHR(i)(Mk)∨Mr(0)
其中,
M
k
M_k
Mk为当前状态,
r
r
r表示下一个流入任务的类型(A或B),
i
i
i是当前状态允许流入的
r
r
r型任务的时间间隔。
S
H
R
i
(
M
k
)
SHR^{i}(M_k)
SHRi(Mk)表示把当前状态中的各冲突向量逻辑右移
i
i
i位。
举个例子:
找出下面这条双功能非线性流水线单独处理A类任务、单独处理B类任务、混合处理两类任务的最优调度方案:
(1)把两个预约表混合起来:
(2)根据预约表求初始冲突矩阵(注意思考为什么是前减后)
a. A类接A类:禁止表为{2,3},所以
C
A
A
=
(
0110
)
C_{AA}=(0110)
CAA=(0110);
b. A类接B类:禁止表为{2,4},所以
C
A
B
=
(
1010
)
C_{AB}=(1010)
CAB=(1010);
c. B类接A类:禁止表为{1,2,4},所以
C
B
A
=
(
1011
)
C_{BA}=(1011)
CBA=(1011);
d. B类接B类:禁止表为{2,3},所以
C
B
B
=
(
0110
)
C_{BB}=(0110)
CBB=(0110)。
因此,初始矩阵为:
(3)由初始冲突矩阵画出状态转换图
(4)由状态转换图找出最优调度方案
只流入A类任务的最优调度为(A.1,A.4)
只流入B类任务的最优调度为(B.1,B.4)
流入A、B类混合任务的最优调度为(B.1,A.3)
还要解决流水处理带来的一些问题:
流水线时空图的另外的画法:
相关(Dependence)是指两条指令之间存在某种依赖关系。
考虑两条指令
i
i
i和
j
j
j,
i
i
i在
j
j
j的前面。
例如,有些流水线处理机只有一个存储器,数据和指令都存放在这个存储器中。在这种情况下,当执行load指令需要存取数时,若又要同时完成其后某条指令的“取指令”,就会发生访存冲突,如下图带阴影的M所示。
为了消除这种冲突,可以在前一条指令访问存储器时,将流水线停顿一个时钟周期,该停顿周期往往被称为“气泡”。用时空图来表示就是:
可以看出,为消除结构冲突引入的停顿将推迟流水线的完成时间,从而影响流水线的性能。由于这种冲突出现的频度不低,因此一般是采用分别设置独立的指令存储器和数据存储器的方法。或者一个存储器,但是采用两个分离的Cache:指令Cache、数据Cache。
例如:
DADD之后的所有指令都要用到DADD指令的计算结果,如图:
DADD指令在其WB段(第5个时钟周期)才将计算结果写入寄存器R1,但是DSUB指令在其ID段(第3个时钟周期)就要从寄存器R1读取该结果,这就是一个数据冲突。若不采取措施防止这一情况发生,则DSUB指令读到的值就是错误的。XOR指令也受到这种冲突的影响,它在第4个时钟周期从R1读出的值也是错误的。而AND指令则可以正常执行,这是因为它在第5个时钟周期的后半拍才从寄存器读数据的,而DADD指令在第5个时钟周期的前半拍已将结果写入寄存器。
考虑两条指令 i i i和 j j j,且 i i i在 j j j之前进入流水线,可能发生的数据冲突有以下几种:
a. 写后读冲突(Read After Write, RAW):指令 j j j用到指令 i i i的计算结果,而且在 i i i将结果写入寄存器之前就去读该寄存器,因而得到的是旧值。对应于数据相关。
b. 写后写冲突(Wirte After Write, WAW):指令
j
j
j和指令
i
i
i的结果寄存器相同,而且
j
j
j在
i
i
i写入之前就对该寄存器进行了写入操作,最后在结果寄存器中留下的是
i
i
i写入的值,而不是
j
j
j写入的值。对应于输出相关。
写后写冲突仅发生在:流水线中不止一个段可以进行写操作;或者指令顺序被重新排列。前面的简单的5段流水线只在WB写了寄存器,所以不会发生写后写冲突。
c. 读后写冲突(Read After Write, RAW):指令 j j j的目的寄存器和指令 i i i的源操作数寄存器相同,而且 j j j在 i i i读取该寄存器之前就先对它进行了写操作,导致 i i i读到的值是错误的。这种冲突是由反相关引起的。这在前面所述的简单5段流水线中不会发生,因为该流水线所有的读操作(在ID段)都在写操作(WB段)之前发生。读后写冲突仅发生在这样的情况下:有些指令的写操作提前了,而有些指令的读操作滞后了;或者指令被重新排序了。
可以使用定向技术减少数据冲突引起的停顿。
对于前面例子中的数据冲突,简单的处理方法是暂停流水线中DADD之后的所有指令,直到DADD指令将计算结果写入寄存器R1之后,再让DADD之后的指令继续执行,但这种暂停会导致性能下降。
可以建立数据旁路,将数据直接从产生的地方送到使用的地方,如图:
需要停顿的数据冲突:
但并不是所有的数据冲突都可以通过定向技术来解决,如图:
DADD要使用LD指令的结果,如虚线所示,这显然是无法实现的。解决办法就是停顿。
依靠编译器解决数据冲突
为了减少停顿,对于无法使用定向技术解决的数据冲突,可以通过在编译时让编译器重新组织指令顺序来消除冲突,这种技术称为“指令调度”(Instruction Scheduling)或“流水线调度”(Pipeline Scheduling)。
举个例子,考虑下面的表达式:
这两个式子编译后形成的代码为:
在这个代码序列中,DADD Ra, Rb, Rc 与 LD Rc, C之间存在数据冲突,DSUB Rd, Re, Rf 与 LD Rf, F 之间存在数据冲突。为了保证流水线能正确执行,必须在指令执行过程中插入两个停顿周期(分别在DADD和DSUB执行前)。
将指令顺序调整为:
再加上相应的数据旁路,就可以消除数据冲突,不必在执行过程中插入任何停顿的周期。
在流水线中,控制冲突可能会比数据冲突造成更多的性能损失,所以同样需要得到很好的处理。
执行分支指令的结果有两种:一种是分支“成功”,PC值改变为分支转移的目标地址;另一种则是“不成功”或者“失败”,这时PC值保持正常递增,指向顺序的下一条指令。对分支指令“成功”的情况来说,是在条件判定和转移地址计算都完成后,才改变PC值的。对于上面的5段流水线来说,改变PC值是在MEM段进行的。
处理分支指令最简单的方法是“冻结”或者“排空”流水线。即一旦在流水线的译码段ID检测到分支指令,就暂停其后所有指令的执行,直到分支指令到达MEM段、确定是否成功并计算出新的PC值为止。然后,按照新的PC值取指令,如下图所示:
在这种情况下,分支指令给流水线带来了三个时钟周期的延迟。而且无论分支成功还是失败,都是一样的排空等待。
分支指令在目标代码中出现的频度是不低的,统计结果表明,每三四条指令就有一条是分支指令。所以排空的处理办法所带来的性能损失是相当大的。
另外采取的办法是:提前判断(或猜测)分支是否成功,尽早计算出分支目标地址。可以把它们提前到ID段完成(比如BEQZ,只需要判断指定寄存器中的数是否为0,ID段已经读取寄存器的同时就可以做,计算出分支目标地址是将PC+IMM<<2,ID段已经取出IR中的立即数,并且能做符号扩展,当然也能做加法),这样就可以把分支延迟减少到一个时钟周期。
进一步减少分支延迟的方法有很多种,下面只介绍三种通过软件来处理的方法:
方法是沿失败的分支继续处理指令,当确定分支失败时,就将分支指令看成一条普通指令,正常流动;当确定分支成功时,就把分支之后取出的指令转换为空操作,并按分支目标地址重新取指令执行
当流水线检测到分支指令后,一旦计算出了分支目标地址,就开始从该目标地址取指令执行。
在上面的5段流水线中,由于判断分支是否成功与分支目标地址计算式在同一流水段完成的,所以这种方法对减少该流水段的分支延迟没有任何好处。但在其它的一些流水线处理机中,特别是哪些具有隐含设置条件码或分支条件更复杂(因而更慢)的流水线处理机中,在确定分支是否成功之前,就能得到分支的目标地址。这时采用这种方法便可以减少分支延迟。
注意:这和预测分支失败不是一种方法,仔细品!
在分支指令后添加延迟槽:
分支指令
延迟槽
后继指令
具体延迟槽里放的指令是由编译器来选择的。实际上,延迟槽能否带来好处完全取决于编译器能否把有用的指令调度到延迟槽中,这也是一种指令调度技术。常用的调度方法有三种:从前调度、从目标处调度、从失败处调度。
举例,如图:
a) 从前调度
把位于分支指令之前的一条独立的指令移到延迟槽。
b)从目标处调度
如图,分支指令之前数据存到了R1寄存器,然后R1寄存器被用于了分支指令的判断,所以不能把分支指令之前的这条指令调到延迟槽。
但是可以将分支指令跳转地址的第一条指令调到延迟槽,然后把分支跳转地址修改为第一条指令所在地址的后面,这实际上是预测了分支会成功,如果失败,相当于延迟槽中的指令就无用了。而且这要求分支地址计算在下一条指令进入之前完成。
c)从失败处调度
即将分支指令后面的第一条指令放入延迟槽,正常运行。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。