赞
踩
指令级并行(Instruction-Level Parallelism,ILP):指指令之间存在的一种并行性,利用它,计算机可以并行执行两条或两条以上的指令。
开发ILP的两种途径:
开发ILP的方法可以分为两大类:
流水线处理机的实际CPI
理想流水线的CPI
加上各类停顿的时钟周期数:
C
P
I
流水线
=
C
P
I
理想
+
停顿
结构冲突
+
停顿
数据冲突
+
停顿
控制冲突
CPI_\text{流水线} = CPI_\text{理想} + \text{停顿}_\text{结构冲突} + \text{停顿}_\text{数据冲突} + \text{停顿}_\text{控制冲突}
CPI流水线=CPI理想+停顿结构冲突+停顿数据冲突+停顿控制冲突
理想CPI
是衡量流水线最高性能的一个指标
IPC:Instructions Per Cycle:每个时钟周期完成的指令条数
基本程序块
循环级并行
相关有三种类型:
可以从两个方面来解决相关问题:
程序顺序:由原来程序确定的在
完全串行方式
下指令的执行顺序。
程序顺序与属性
控制相关:并不是一个必须严格保持的关键属性(不是提高性能的主要限制)。
对于正确地执行程序来说,必须保持的最关键的两个属性是:数据流和异常行为。
有时,不遵守控制相关既不影响异常行为,也不改变数据流。
例如:对于以下程序
DADDU R1,R2,R3
BEQZ R12,Skipnext
DSUBU R4,R5,R6
DADDU R5,R4,R9
Skipnext:OR R7,R8,R9
假设我们知道R4在Skipnext
之后不再被使用,而且DSUBU
不会产生异常,那么就可以把DSUBU
移到分支指令之前。这是因为这个移动不会改变数据流。
如果分支成功,尽管按原程序DSUBU
指令是不该执行的,但现在执行了也没关系,因为它不会影响程序的执行结果。
静态调度与动态调度的对比
静态调度 | 动态调度 | |
---|---|---|
调度发生时间 | 在编译期间进行代码调度和优化 | 在程序的执行过程中 |
依靠 | 依靠编译器对代码进行静态调度,以减少相关和冲突 | 依靠专门硬件对代码进行调度 |
调度方式 | 通过把相关的指令拉开距离来减少可能产生的停顿 | 减少数据相关导致的停顿 |
动态调度的优缺点
优点 | 缺点 |
---|---|
能够处理一些在编译时情况不明的相关(比如涉及到存储器访问的相关),并简化了编译器 | 硬件复杂性的显著增加为代价 |
能够使本来是面向某一流水线优化编译的代码在其它的流水线(动态调度)上也能高效地执行 |
静态与动态流水线的时空图
到目前为止我们所使用流水线(5段流水线)的最大的局限性:
例:考虑下面一段代码:
DIV F4,F0,F2
ADD F10,F4,F6
SUB F12,F6,F14
ADD
指令与DIV
指令关于F4
相关,导致流水线停顿。SUB
指令与流水线中的任何指令都没有关系,但也因此受阻。在5段流水线中,结构冲突和数据冲突都是在译码(ID)段
进行检测的。只有当既没有结构冲突也没有数据冲突时,指令才能够流出。为了使上述指令序列中的SUB指令能继续执行下去,必须把指令流出的工作拆分为两步
只要检测到没有结构冲突,就可以让指令流出。并且流出后的指令一旦其操作数就绪就可以立即执行。
⚠️修改后的流水线是乱序执行
即指令的执行顺序与程序顺序不相同。
同样,指令的完成也是乱序完成的,即指令的完成顺序与程序顺序不相同。
为了支持乱序执行,将前述5段流水线的译码(ID)段
细分为两个段。
指令的流出仍然是按序流出(In-order Issue)的。
但是,它们在读操作数段可能停顿和互相跨越,因而进入执行段时就可能已经是乱序的了。
修改前后的时空图
DIV F4,F0,F2 | IF | ID | EX | MEM | WB | |||||
---|---|---|---|---|---|---|---|---|---|---|
ADD F10, F4, F6 | stall | stall | stall | IF | ID | EX | MEM | WB | ||
SUB F12, F6, F14 | IF | ID | EX | MEM | WB |
DIV F4,F0,F2 | IF | ID | EX | MEM | WB | |||||
---|---|---|---|---|---|---|---|---|---|---|
ADD F10, F4, F6 | stall | stall | stall | IF | ID | EX | MEM | WB | ||
SUB F12, F6, F14 | IF | ID | EX | MEM | WB |
在前述5段流水线中,是不会发生
WAR
冲突和WAW
冲突的。但乱序执行就使得它们可能发生了。考虑以下代码:
DIV F10, F0, F2
ADD F10, F4, F6
SUB F6, F8, F14
DIV
指令与ADD
指令存在输出相关ADD
指令和SUB
指令存在反相关动态调度的流水线支持多条指令同时处于执行当中。
i
导致发生异常时,处理机的现场(状态)与严格按程序顺序执行时指令i
的现场不同。i
导致发生异常时,处理机的现场(状态)与严格按程序顺序执行时指令i
的现场相同。要设置一定数量的后援寄存器,把流水线中所有指令的执行结果和现场保存下来。成本高,现在都这样实现。i
之后的指令;i
之前的指令。精确断点法
不精确断点法:
采用不精确断点法可能会发生如下两个问题:
CDC 6600计算机最早采用此功能
译码段ID
分解成了两个段:流出段和读操作数段,以避免当某条指令在ID段被停顿时挡住后面无关指令的流动。要发挥指令乱序执行的好处,必须有多条指令同时处于执行阶段。
16
个独立的功能部件
为了讨论的简单,假设所考虑的处理器有2个乘法器、1个加法器、1个除法部件和1个整数部件。
每条指令的执行过程
(EX段)分为4段
(主要考虑浮点操作 )
WAW
冲突RAW
冲突,并导致指令可能乱序开始执行。WAR
冲突。如果不存在,或者原有的WAR
冲突已消失,记分牌就通知功能部件把结果写入目的寄存器,并释放该指令使用的所有资源。检测到WAR
冲突的情况以及解决
由三部分构成
分析下列代码运行过程中记分牌保存的信息
L.D F6, 34(R2)
L.D F2, 45(R3)
MULT.D F0, F2, F4
SUB.D F8, F6, F2
DIV.D F10, F0, F6
ADD.D F6, F8, F2
情况一:第一条指令L.D
执行完毕;第二条指令L.D
执行完毕,但没有写入目的寄存器F2
,记分牌状态表如下:
在上图的指令状态表中:
L.D
指令已经完全执行完,其结果已经写人了F6
;L.D
指令也已经执行完,但是结果还没有写人目的寄存器F2
;L.D
指令与MULT.D
和SUB.D
指令之间存在关于寄存器F2
的先写后读(RAW)相关
,因此MULT.D
和SUB. D
在流出段等待,不能进入流水线的读操作数段。MULT.D
与DIV.D
之间存在关于寄存器F0
的RAW
相关,因此DIV.D
也只能在流出段等待。ADD.D
与指令SUB.D
之间存在关于加法器的结构相关
,因此后面的ADD.D连流出都不能做,必须等到前面SUB.D指令全部执行完毕、释放加法器后才能够流出。在上图的**功能部件状态表**中:
整数(Integer)部件
在乘法1(Mult1)部件
乘法2(Mult2)部件
其余部件的分析类似
在上图的结果寄存器状态表中:
为更进一步分析,假设复电流水线各部件的延迟如下:
加法需2个时钟周期
乘法需10个时钟周期
除法需40个时钟周期
情况二:给出MULT.D
和DIV.D
准备写结果之前的记分牌状态。
MULT.D
将要写结果时记分牌的状态从上图中可以知道,在MULT. D
准备写结果之前,由于DIV.D
指令与MULT.D
指
令之间存在关于寄存器F0的RAW相关
,因此在MULT. D
指令完成写结果之前,DIV.D
指令被阻塞在流出段而无法进人读操作数段。
同时由于ADD.D
指令与DIV.D
指令之间存在关于寄存器F6的WAR相关
,因此在DIV.D指令从F6中读出操作数之前,ADD. D指令被阻塞在执行段,无法进入写结果段。
程序段执行到DIV.D
将要写结果时记分牌的状态
在上图中
DIV.D
指令执行需要40
个时钟周期,其后的ADD.D
指令只需要两个
时钟周期DIV.D
准备写结果之前,这条指令前面的指令已经全部执行完毕DIV.D
和ADD. D
之间存在的WAR相关
在DIV.D
读走源操作数后就已经消失了ADD.D
有足够的时间完成所有操作,所以就只剩下DIV.D
指令没有完成写结果操作。为了表述算法的方便,约定:
FU
:表示当前指令所要用的功能部件;D
:目的寄存器的名称;S1、S2
:源操作数寄存器的名称;Op
:要进行的操作;FU
的
F
j
F_{j}
Fj字段(其他字段依此类推);Result[D]
:结果寄存器状态表中与寄存器D
相对应的内容。其中存放的是将把结果写入寄存器D的功能部件的名称。指令流出
//进入条件: not Busy[FU] & not Result[D]; // 功能部件空闲且没有写后写(WAW)冲突。 //计分牌内容修改: Busy[FU] ← yes; // 把当前指令的相关信息填入功能部件状态表。 Op[FU] ← Op; // 记录操作码。 Fi[FU] ← D; // 记录目的寄存器编号。 Fj[FU] ← S1; // 记录第一个源寄存器编号。 Fk[FU] ← S2; // 记录第二个源寄存器编号。 Qj[FU] ← Result[S1]; // 记录将产生第一个源操作数的部件。 Qk[FU] ← Result[S2]; // 记录将产生第二个源操作数的部件。 //如果Qj[FU]为“no”,就表示没有操作部件要写S1,数据可用。 //置Rj[FU]为“yes”。否则置Rj[FU]为“no”。 Rj[FU] ← not Qj[FU]; // 置第一个源操作数是否可用的标志。 Rk[FU] ← not Qk[FU]; // 置第二个源操作数是否可用的标志。 Result[D] ← FU; // D是当前指令的目的寄存器。功能部件FU将把结果写入D。
指令流出
//进入条件:
Rj[FU] & Rk[FU]; // 两个源操作数都已就绪。
//计分牌内容修改:
Rj[FU] ← no; // 已经读走了就绪的第一个源操作数。
Rk[FU] ← no; // 已经读走了就绪的第二个源操作数。
Qj[FU] ← 0; // 不再等待其他FU的计算结果。
Qk[FU] ← 0;
执行
//结束条件
//功能部件操作结束。
写结果
//进入条件:不存在WAR冲突。
∀f((Fj[f] ≠ Fi[FU] or Rj[f] = no)
& (Fk[f] ≠ Fi[FU] or Rk[f]=no))
//计分牌内容修改:
// 如果有指令在等待该结果(作为第一源操作数),则将其Rj置为“yes”,表示数据可用。
∀f(if Qj[f] = FU then Rj[f] ← yes);
// 如果有指令在等待该结果(作为第二源操作数),则将其Rk置为“yes”,表示数据可用。
∀f(if Qk[f]=FU then Rk[f]←yes);
Result(Fi[FU]) ← 0; // 释放目的寄存器Fi[FU]。
Busy[FU] = no; // 释放功能部件FU。
程序代码中可开发的并行性,即是否存在可以并行执行的不相关的指令。
记分牌的容量。
功能部件的数目和种类。
反相关和输出相关。
问题(2)和(3)可通过增加记分牌的容量和功能部件的数量来解决,但这会导致处理器成本增加,并可能影响系统时钟周期时间。
在采用动态调度的处理器中,WAW和WAR冲突会增多,因为乱序流出的指令在流水线中会引起更多的名相关。
如果在动态调度中采用分支预测技术,就会出现循环的多个迭代同时执行,名相关将更加严重。
IBM 360/91首先采用了Tomasulo算法。
RAW冲突
的可能性减少到最小(引入延迟);WAR冲突
和WAW冲突
。IBM 360/91的设计目标是基于整个360系列的统一指令系统和编译器来实现高性能,而不是设计和利用专用的编译器来提高性能。与此同时, 需要更多地依赖于硬件。
IBM 360体系结构只有4个双精度浮点寄存器,限制了编译器调度的有效性。
360/91的访存时间和浮点计算时间都很长。
其中各个功能部件介绍如下:
1.保留站
(reservation station)
每个保留站中保存一条已经流出并等待到本功能部件执行的指令(相关信息)。
存储内容包括:操作码、操作数以及用于检测和解决冲突的信息。
对源操作数的处理
每个保留站都有一个标识字段,唯一地标识了该保留站。
保留站的类型
2.公共数据总线CDB
3.load缓冲器和store缓冲器
存放读/写存储器的数据或地址
load缓冲器的作用有3个:
store缓冲器的作用有3个:
4.浮点寄存器FP
5.指令队列
6.运算部件
实现:
寄存器换名是通过保留站和流出逻辑来共同完成的。
当指令流出时,如果其操作数还没有计算出来,则将该指令中相应的寄存器号换名为将产生这个操作数的保留站的标识。
结论:
由于Tomasulo算法采用分布的保留站,因此具有以下特点:
使用Tomasulo算法的流水线需3段(均在EX段
):
1.流出
:从指令队列的头部取一条指令。
如果该指令的操作所要求的保留站有空闲的,就把该指令送到该保留站(设为r)。
如果其操作数在寄存器中已经就绪,就将这些操作数送入保留站r。
如果其操作数还没有就绪,就把将产生该操作数的保留站的标识送入保留站r。
一旦被记录的保留站完成计算,它将直接把数据送给保留站r。
消除WAR以及WAW冲突
在这一阶段,通过对寄存器换名(换成保留站的标识)和对操作数进行缓冲(就绪的操作数直接存入保留站),消除了WAR冲突
。
此外,这一阶段还完成了对目的寄存器的预约工作,将其设置为接收保留站r的结果,这实际上相当于完成了写操作(预约)。由于指令是按照顺序流出的,当出现多条指令写同一个结果寄存器时,最后留下的结果一定是最后一条指令的,即消除了WAW冲突
。
如果没有空闲的保留站,指令就不能流出。
结构冲突
2.执行
消除RAW冲突
推迟执行
的方式解决RAW冲突
。3.写结果
O p Op Op:要对源操作数进行的操作
Q
j
,
Q
k
Q_{j},Q_{k}
Qj,Qk:将产生源操作数的保留站号
V j , V k V_{j},V_{k} Vj,Vk:源操作数的值
B u s y Busy Busy:为“yes”表示本保留站或缓冲单元“忙”
A
A
A:仅load
和store
缓冲器有该字段。开始是存放指令中的立即数字段,地址计算后存放有效地址。
Q j Q_{j} Qj:寄存器状态表
对下列指令序列给出当第一条指令完成并写入结果时,Tomasulo算法所用的各信息表中的内容
L.D F6,34(R2)
L.D F2,45(R3)
MUL.D F0,F2,F4
SUB.D F8,F2,F6
DIV.D F10,F0,F6
ADD.D F6,F8,F2
图中所有的指令均已流出,但只有第一条load指令执行完毕并将结果写到CDB上。
第二条load指令已经完成有效地址计算,正在等待存储器的响应。
用Regs[]表示寄存器组,用Mem[]表示存储器。
这里,刚刚流出的ADD.D指令与前一条指令DIV.D有一个WAR冲突,但它可以先于DIV. D完成而不会导致错误。
各符号的意义
r
:分配给当前指令的保留站或者缓冲器单元编号;
rd
:目标寄存器编号;
rs、rt
:操作数寄存器编号;
imm
:符号扩展后的立即数;
RS
:保留站;
result
:浮点部件或load缓冲器返回的结果;
Qi
:寄存器状态表;
Regs[ ]
:寄存器组;
与rs对应的保留站字段:Vj
,Qj
与rt对应的保留站字段:Vk
,Qk
Qi、Qj、Qk
的内容或者为0,或者是一个大于0的整数。
Qi
为0表示相应寄存器中的数据就绪。Qj、Qk
为0表示保留站或缓冲器单元中的Vj
或Vk
字段中的数据就绪。正整数
时,表示相应的寄存器、保留站或缓冲器单元正在等待结果。1. 指令流出
// 进入条件:有空闲保留站(设为r) // 操作和状态表内容修改: if (Qi[rs] ≠ 0) // 检测第一操作数是否就绪 { //第一操作数没有就绪,进行寄存器换名,即把将产生该操作数的保留站的编号放入当前保留站的Qj。 //该编号是一个大于0的整数。 RS[r].Qj ← Qi[rs]; } else { RS[r].Vj ← Regs[rs]; //第一操作数就绪。把寄存器rs中的操作数取到当前保留站的Vj。 RS[r].Qj ← 0 //置Qj为0,表示当前保留站的Vj中的操作数就绪 。 } if (Qi[rt] ≠ 0) // 检测第二操作数是否就绪 { //第二操作数没有就绪,进行寄存器换名,即把将产生该操作数的保留站的编号放入当前保留站的Qk。 //该编号是一个大于0的整数。 RS[r].Qk ← Qi[rt]; } else { RS[r].Vk ← Regs[rt]; //第二操作数就绪。把寄存器rt中的操作数取到当前保留站的Vk。 RS[r].Qj ← 0; //置Qk为0,表示当前保留站的Vk中的操作数就绪 。 } RS[r].Busy ← yes; //置当前保留站为“忙” RS[r].Op ← Op; //设置操作码 Qi[rd] ← r; // 把当前保留站的编号r放入rd所对应的寄存器状态表项,以便rd将来接收结果。
// 进入条件:缓冲器有空闲单元(设为r)
// 操作和状态表内容修改:
if (Qi[rs] ≠ 0) // 检测第一操作数是否就绪
{
//第一操作数没有就绪,进行寄存器换名,即把将产生该操作数的保留站的编号存入当前缓冲器单元Qj。
//该编号是一个大于0的整数。
RS[r].Qj ← Qi[rs];
}
else
{
RS[r].Vj ← Regs[rs]; //第一操作数就绪。把寄存器rs中的操作数取到当前缓冲器单元的Vj。
RS[r].Qj ← 0 //置Qj为0,表示当前缓冲器单元的Vj中的操作数就绪 。
}
RS[r].Busy ← yes; // 置当前缓冲器单元为“忙”
RS[r].A ← Imm; // 把符号位扩展后的偏移量放入当前缓冲器单元的A
对于load指令
// 把当前缓冲器单元的编号r放入load指令的目标寄存器rt所对应的寄存器状态表项,以便rt将来接收所取的数据。
Qi[rt] ← r;
对于store指令
if (Qi[rt] ≠ 0) // 检测要存储的数据是否就绪
{
// 该数据尚未就绪,进行寄存器换名,即把将产生该数据的保留站的编号放入当前缓冲器单元的Qk。
RS[r].Qk ← Qi[rt];
}else
{
RS[r].Vk ← Regs[rt]; // 该数据就绪,把它从寄存器rt取到store缓冲器单元的Vk
RS[r].Qk ← 0; // 置Qk为0,表示当前缓冲器单元的Vk中的数据就绪。
}
2.执行
// 进入条件:(RS[r].Qj = 0)且(RS[r].Qk = 0) // 两个源操作数就绪
// 操作和状态表内容修改:进行计算,产生结果 。
// 进入条件:(RS[r].Qj = 0)且r成为load/store缓冲队列的头部
// 操作和状态表内容修改:
RS[r].A ← RS[r].Vj + RS[r].A; //计算有效地址
//对于load指令,在完成有效地址计算后,还要进行:
从Mem[RS[r].A]读取数据; //从存储器中读取数据
3.写结果
// 进入条件:保留站r执行结束,且CDB就绪。 // 操作和状态表内容修改: ∀x (if(Qi[x] = r) // 对于任何一个正在等该结果的浮点寄存器x { Regs[x] ← result; // 向该寄存器写入结果 Qi[x] ← 0; // 把该寄存器的状态置为数据就绪 } ∀x (if(RS[x].Qj = r) // 对于任何一个正在等该结果第作为第一操作数的保留站x { RS[x].Vj ← result; // 向该保留站的Vj写入结果 RS[x].Qj ← 0; // 置Qj为0,表示该保留站的Vj中的操作数就绪 } ∀x (if(RS[x].Qk = r) // 对于任何一个正在等该结果作为第二操作数的保留站x { RS[x].Vk ← result; // 向该保留站的Vk写入结果 RS[x].Qk ← 0; // 置Qk为0,表示该保留站的Vk中的操作数就绪。 } RS[r].Busy ← no; // 释放当前保留站,将之置为空闲状态。
// 进入条件:保留站r执行结束,且RS[r].Qk = 0 // 要存储的数据已经就绪
// 操作和状态表内容修改:
Mem[RS[r].A] ← RS[r].Vk // 数据写入存储器,地址由store缓冲器单元的A字段给出。
RS[r].Busy ← no; // 释放当前缓冲器单元,将之置为空闲状态。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。