1 动态流水线技术的由来
为什么会发明动态流水线?首先我们来了解一下静态流水线的一个弊端,我们都知道简单的静态流水线,它们都是使用顺序指令发射与执行,一旦出现一条指令停顿在流水线中,后续的指令都不能执行,即使后面有些指令不依赖这条阻塞的指令,那也只能乖乖阻塞等待,效率大大降低,所以动态流水线应运而生。
举个简单的例子帮助理解:简单的静态流水线就像是一条单向单车道且无法超车,一旦前面的车出现车祸,那么后面所有的车都被堵住;而动态流水线就像是多车道,如果前面有一辆车出现车祸,其后面无关的车辆可以通过其他车道超车前进。
2 浅谈指令调度技术
什么叫做指令调度技术呢?指令调度技术的一个前提是不改变程序正确性,通过改变指令的执行次序来避免由于指令相关引起的流水线阻塞。
* 静态指令调度
由程序猿或者编译器在程序执行之前进行的指令调度,那多少有点废程序猿了。
主要的方法有哪些呢?
1.循环展开——将来自不同迭代的指令放在一起调度,以此来消除数据使用停顿,但是如果代码规模比较大的话,导致cache的命中率会下降,而且可能导致寄存器紧缺。
2.寄存器重命名——可以消除WAW(写后写)和WAR(读后写)相关,但需要更多的寄存器。
* 动态指令调度
在程序执行过程中由硬件自动进行的指令调度,这不程序猿就可以早点下班了嘛,下面会具体介绍。
3 冒险
为什么放在这里介绍冒险呢?对,就是要提醒你一共有3种流水线冒险,其中数据冒险还有3种情况。
* 结构冒险
其实结构冒险可以先不管,因为不常见。在现代处理器中,结构冒险主要发生在不太常用的特殊用途功能单元(例如浮点除法)。
* 控制冒险
官方是这么说的:分支指令及其他改变程序计数器的指令实现流水化时可能导致控制冒险。
可能你看的云里雾里,稍微举个例子解释一下,就拿if语句来举例。
if p1{
s1; // s1控制依赖于p1
}
if p2{
s2; // s2控制依赖于p2,但不控制依赖于p1
}
这个例子中,我们不能把s1中的一条指令拿出来移到这个if语句之前,因为s1是控制依赖于p1的;另外我们也不能把s2中的指令移到s1中,因为本来s2就不控制依赖于p1,移动的话就会产生控制依赖。
* 数据冒险
- RAW(写后读)又称真相关——本应该先写后读,但j试图在i写入一个源位置之前读取来该位置的值,则会导致j错误的读取了旧值。
- WAW(写后写) 又称输出相关——本应该先i写后j写,但j试图在i写一个操作数之前写这个操作数,也就是写入顺序出错,最后的结果出错。
- WAR(读后写)又称反相关——本应该先读后写,但j尝试在i读取一个操作数之前写这个操作数,导致结果出错。
4 动态调度
4.1 思想
动态调度,就是在指令的执行过程中,由硬件自动对指令的执行次序进行调度以避免由于数据相关引起的流水线阻塞。总结就一句话:前面指令的阻塞不影响后面指令的继续前进。
4.2 实现方法
先看一段代码:
fdiv.d f0 , f2 , f4 //浮点除法
fadd.d f10 , f0 , f8 //浮点加法
fsub.d f12 , f8 , f14 //浮点减法
其中div和add中因为f0存在数据依赖,所以add必须阻塞等待div执行完成,但是sub不依赖于div和add,可以先行执行,所以如何才能让sub先行执行呢?
将发射过程分为两个部分:
(1)发射——指令译码,检查结构冒险
(2)读取操作数——一直等到没有数据冒险后,然后读取操作数
那么疑问又来了,为什么我们要分成两个部分?很好,那这又得说回简单的静态五级流水了,在静态的五级流水中,在ID(译码阶段)检查结构冒险和数据冒险,什么意思勒,就是一定要在无冒险执行时,才会发射,那么也就是说,如果存在冒险,那么就不会发射,就只能等待,上面例子中sub指令就得一直等等等。
那么如果我们分成两部分,还是正常顺序发射,但如果某条指令需要等待操作数的话就先把它暂存到一个地方(这个地方叫保留站),等待操作数可用的时候就立即执行,这样就可以腾出指令执行的通路,后面不相关的指令就可以继续执行,这样就实现了乱序执行。
4.2.1 CDC6600记分牌(scoreboarding)
上面提到了我们通过保留站实现了乱序执行(out-of-order execution),那么可以确定的是,乱序执行的结果就是乱序完成(out-of-order completion)。
记分牌的主要思想:
(1) 发射前检测结构相关和WAW相关
(2)读操作数前检测RAW相关(即仅在操作数可用时才执行该指令)
(3)写结果前处理WAR相关
记分牌的缺点:
(1)没有定向数据通路
(2)指令窗口较小,仅局限于基本块内的调度
(3)结构冲突时不能发射
(4)WAR相关是通过等待解决的
(5)WAW相关时,不会进入Issue(发射)阶段
(6)功能部件数较少
4.2.2 Tomasulo算法
Tomasulo算法的核心思想:
(1)跟踪指令的操作数何时可用,以将RAW冒险降至最低;
(2)在硬件中引入寄存器重命名功能,将WAW和WAR冒险降至最低。
另外,还可以对Tomasulo算法进行扩展,用来处理推测,通过预测一个分支的输出、执行预测目标地址的指令、在预测错误时采取纠正措施,降低控制依赖的影响。 Tomasulo算法还可以处理跨越分支的重命名问题。
寄存器重命名功能实现:
寄存器重命名功能由保留站(reservation station)提供。
基本思想:保留站在一个操作数可用时马上提取并缓冲它,这样就不再需要从寄存器中获取该操作数。此外,等待执行的指令会指定保留站为自己提供输入。最后,在对寄存器连续进行写操作时并且重叠执行时,实际只会用到最后一个操作更新寄存器。
为什么使用保留站?
我们有个疑问,为什么使用保留站,而不是使用一个集中式的寄存器堆呢?
原因有两个:
(1)冒险检测和执行控制是分布式的:每个功能单元保留站中保存的信息,决定了一条指令什么时候开始在该单元中执行;
(2)结果将直接从缓冲它们的保留站中传递给功能单元,而不需要经过寄存器。是通过公共数据总线(CDB)完成的,它允许同时载入所有等待一个操作数的单元。
保留站中几个字段解释:
·Op—— 对源操作数S1和S2执行的运算;
·Qj 、Qk —— 将生成相应源操作数的保留站;当取值为0时,表明已经可以在Vj或Vk中获取源操作数,或者不需要源操作数;
·Vj 、Vk —— 源操作数的值。注意,对于每个操作数,V字段或Q字段中只有一个是有效的
· Busy —— 指明这个保留站及其相关功能单元已被占用;
寄存器堆中的字段:
· Qi —— 空表示寄存器值可用,否则保存产生寄存器结果的保留站号。
执行流程解释:
这里为了帮助理解,我用胡伟武老师《体系结构》的课件中的一个例子:
DIV.D F0,F1,F2
MUL.D F3,F0,F2
ADD.D F0,F1,F2
MUL.D F3,F0,F2
指令队列按照FIFO顺序维护,以确保顺序发射。如果有一个匹配的保留站为空,则将这条指令发射到这个站中,如果操作数值已经存在于寄存器,也一并发送到站中。如果没有空闲保留站,则存在结构冒险,该指令会停顿,直到有保留站或缓冲区被释放为止。(下图中的左上角的标号不用管,标号的时候略过5号了,顺序是对滴~)
总结
后续再更新ROB和分支预测等内容,觉得写的还可以,点赞关注吧。。