提高流水线执行效率的技术

一、提高流水线执行效率

看懂这一章节的前提是,掌握经典的单发射五级流水线原理,《深入理解计算机系统》第四章中有详细的讲解,配合《计算机体系结构基础》第3版一起食用,读CSAPP第四章节时做的笔记:

我们通常以应用的执行时间来衡量一款处理器的性能,应用的执行时间 = 指令数 * CPI(Cycles Per Instruction,每指令执行周期数) * 时钟周期。 当算法、程序、指令系统、编译器都确定之后,一个应用的指令数就确定下来了。时钟周期与结构设计、电路设计、生产工艺以及工作环境都有关系。所以要提高流水线效率,需重点关注如何降低流水线CPI。流水线处理器实际的CPI等于指令的理想执行周期数加上由于指令相关引起的阻塞周期数:

流水线CPI = 理想CPI + 结构相关阻塞周期数 + RAW阻塞周期数 + WAR阻塞周期数

从上面的公式可知,要想提高流水线效率(即降低Pipeline CPI),可以从降低理想CPI和降低各类流水线阻塞这些方面入手。

常见提高流水线执行效率的技术有:多发射数据通路、动态调度、分支预测、高速缓存等。

二、多发射数据通路

首先讨论如何降低理想CPI。最直观的方法就是让处理器中每级流水线都可以同时处理更多的指令,这被称为多发射数据通路技术。例如双发射流水线意味着每一拍用PC从指令存储器中取两条指令,在译码级同时进行两条指令的译码、读源寄存器操作,还能同时执行两条指令的运算操作和访存操作,并同时写回两条指令的结果。那么双发射流水线的理想CPI就从单发射流水线的1降至0.5。

要在处理器中支持多发射,首先就要将处理器中的各种资源翻倍,包括采用支持双端口的存储器。其次还要增加额外的阻塞判断逻辑,当同一个时钟周期执行的两条指令存在指令相关时,也需要进行阻塞。包括数据相关、控制相关和结构相关在内的阻塞机制都需要改动。我们来观察几条简单指令在双发射流水线中的时空图,如图9.18所示。

在这里插入图片描述

在上图中,为了流水线控制的简化,只有同一级流水线的两条指令都不被更早的指令阻塞时,才能让这两条指令一起继续执行,所以第6条指令触发了陪同阻塞。

多发射数据通路技术虽然从理论上而言可以大幅度降低处理器的CPI,但是由于各类相关所引起的阻塞影响,其实际执行效率是要大打折扣的。所以还要进一步从减少各类相关引起的阻塞这个方面入手来提高流水线的执行效率。

三、动态调度

如果我们用道路交通来类比的话,多发射数据通路就类似于把马路从单车道改造为多车道,但是这个多车道的马路有个奇怪的景象——速度快的车(如跑车)不能超过前面速度慢的车(如马车),即使马车前面的车道是空闲的。直觉上我们肯定觉得这样做效率低,只要车道有空闲,就应该允许后面速度快的车超过前面速度慢的车。这就是动态调度的基本出发点。用本领域的概念来描述动态的基本思想就是:把相关的解决尽量往后拖延,同时前面指令的等待不影响后面指令继续前进。下面我们通过一个例子来加深理解:假定现在有一个双发射流水线,所有的运算单元都有两份,那么在执行下列指令序列时:

div.w $r3, $r2, $r1
add.w $r5, $r4, $r3
sub.w $r8, $r7, $r6

由于除法单元采用迭代算法实现,所以div.w指令需要多个执行周期,与它有RAW(写后读)相关的add.w指令最早也只能等到div.w指令执行完毕后才能开始执行。但是sub.w指令何时可以开始执行呢?可以看到sub.w指令与前两条指令没有任何相关,采用动态调度的流水线就允许sub.w指令越过前面尚未执行完毕的div.w指令和add.w指令,提前开始执行。因为sub.w是在流水线由于指令间的相关引起阻塞而空闲的情况下“见缝插针”地提前执行了,所以这段程序整体的执行延迟就减少了。

3.1 保留站(发射队列)

要完成上述功能,需要对原有的流水线做一些改动。首先,要将原有的译码阶段拆分成“译码”和“读操作数”两个阶段。译码阶段进行指令译码并检查结构相关,随后在读操作数阶段则一直等待直至操作数可以读取。处在等待状态的指令不能一直停留在原有的译码流水级上,因为这样它后面的指令就没法前进甚至是进入流水线,更不用说什么提前执行了。所以我们会利用一个结构存放这些等待的指令,这个结构被称为保留站(发射队列)。除了存储指令的功能,保留站还要负责控制其中的指令何时去执行,因此保留站中还会记录下描述指令间相关关系的信息,同时监测各条指令的执行状态。如果指令是在进入保留站前读取寄存器,那么保留站还需要监听每条结果总线,获得源操作数的最新值。

保留站位置如下图所示,译码并读寄存器的指令进入保留站,保留站会每个时钟周期选择一条没有被阻塞的指令,送往执行逻辑,并退出保留站,这个动作称为“发射”。

在这里插入图片描述
保留站调度算法的核心在于“挑选没有被阻塞的指令”。从保留站在流水线所处的位置来看,保留站中的指令不可能因为控制相关而被阻塞。结构相关所引起的阻塞的判定条件也是直观的,即检查有没有空闲的执行部件和空闲的发射端口。但是在数据相关所引起的阻塞的处理上,存在着不同的设计思路。

下面来两个例子解释保留站如何处理数据相关所引起的阻塞:

div.w $r3, $r2, $r1
add.w $r5, $r4, $r3
sub.w $r4, $r7, $r6

add.w指令和sub.w指令之间存在WAR相关,在乱序调度的情况下,sub.w指令自身的源操作数不依赖于div.wadd.w指令,可以读取操作数执行得到正确的结果。那么这个结果能否在执行结束后就立即写入寄存器呢?回答是否定的。假设sub.w执行完毕的时候,add.w指令因为等待div.w指令的结果还没有开始执行,那么sub.w指令如果在这个时候就修改了$r4寄存器的值,那么等到add.w开始执行时,就会产生错误的结果。

WAW相关的例子与上面WAR相关的例子很相似,如下面的指令序列:

div.w $r3, $r2, $r1
add.w $r5, $r4, $r3
sub.w $r5, $r7, $r6

add.w指令和sub.w指令之间存在WAW相关,在乱序调度的情况下,sub.w指令可以先于add.w指令执行,如果sub.w执行完毕的时候,add.w指令因为等待div.w指令的结果还没有开始执行,那么sub.w指令若是在这个时候就修改了$r5寄存器的值,那就会被add.w指令执行完写回的结果覆盖掉。从程序的角度看,sub.w后面的指令读取$r5寄存器会得到错误的结果。

3.2 寄存器重命名

上面的例子解释了WAR和WAW相关在动态调度流水线中是怎样产生冲突的。如何解决呢?阻塞作为解决一切冲突的普适方法肯定是可行的。方法就是如果保留站判断出未发射的指令与前面尚未执行完毕的指令存在WAR和WAW相关,就阻塞其发射直至冲突解决。历史上第一台采用动态调度流水线的CDC6000就是采用了这种解决思路,称为记分板办法。

事实上,WAR和WAW同RAW是有本质区别的,它们并不是由程序中真正的数据依赖关系所引起的相关关系,而仅仅是由于恰好使用具有同一个名字的寄存器所引起的名字相关。打个比方来说,32项的寄存器文件就好比一个有32个储物格的储物柜,每条指令把自己的结果数据放到一个储物格中,然后后面的指令依照储物格的号(寄存器名字)从相应的格子中取出数据,储物柜只是一个中转站,问题的核心是要把数据从生产者传递到指定的消费者,至于说这个数据通过哪个格子做中转并不是绝对的。WAR和WAW相关产生冲突意味着两对“生产者-消费者”之间恰好准备用同一个格子做中转,而且双方在“存放-取出”这个动作的操作时间上产生了重叠,所以就引发了混乱。如果双方谁都不愿意等(记分板的策略)怎么办?再找一个不受干扰的空闲格子,后来的一方换用这个新格子做中转,就不用等待了。这就是寄存器重命名技术。通过寄存器重命名技术,可以消除WAR和WAW相关。例如,存在WAR和WAW相关指令序列:

div.w   $r3, $r2, $r1
add.w   $r5, $r4, $r3
sub.w   $r3, $r7, $r6
mul.w   $r9, $r8, $r3

可以通过寄存器重命名变为:

div.w   $r3,$r2,$r1
add.w   $r5,$r4,$r3
sub.w   $r10,$r7,$r6
mul.w   $r9,$r8,$r10

重命名之后就没有WAR和WAW相关了。

1966年,Robert Tomasulo在IBM 360/91中首次提出了对于动态调度处理器设计影响深远的Tomasulo算法。该算法在CDC6000记分板方法基础上做了进一步改进。面对RAW相关所引起的阻塞,两者解决思路是一样的,即将相关关系记录下来,有相关的等待,没有相关的尽早送到功能部件开始执行。但是Tomasulo算法实现了硬件的寄存器重命名,从而消除了WAR和WAW相关,也就自然不需要阻塞了。

3.3 重排序缓冲(ROB)

在流水线中实现动态调度,还有最后一个需要考虑的问题——精确异常。在处理异常时,发生异常的指令前面的所有指令都执行完(修改了机器状态),而发生异常的指令及其后面的指令都没有执行(没有修改机器状态)。那么在乱序调度的情况下,指令已经打破了原有的先后顺序在流水线中执行了,“前面”“后面”这样的顺序关系从哪里获得呢?还有一个问题,发生异常的指令后面的指令都不能修改机器的状态,但是这些指令说不定都已经越过发生异常的指令先去执行了,怎么办呢?

上面两个问题的解决方法是:在流水线中添加一个重排序缓冲(ROB)来维护指令的有序结束,同时在流水线中增加一个“提交”阶段。指令对机器状态的修改只有在到达提交阶段时才能生效(软件可见),处于写回阶段的指令不能真正地修改机器状态,但可以更新并维护一个临时的软件不可见的机器状态。ROB是一个先进先出的有序队列,所有指令在译码之后按程序顺序进入队列尾部,所有执行完毕的执行从队列头部按序提交。提交时一旦发现有指令发生异常,则ROB中该指令及其后面的指令都被清空。发生异常的指令出现在ROB头部时,这条指令前面的指令都已经从ROB头部退出并提交了,这些指令对于机器状态的修改都生效了;异常指令和它后面的指令都因为清空而没有提交,也就不会修改机器状态。这就满足了精确异常的要求。

3.4 总结

保留站和重排序缓冲用来临时存储指令以使指令在流水线中流动更加通畅,重命名寄存器用来临时存储数据以使数据在流水线流动更加通畅。保留站、重排序缓冲、重命名寄存器都是微结构中的数据结构,程序员无法用指令来访问,是结构设计人员为了提高流水线效率而用来临时存储指令和数据的。其中,保留站把指令从有序变为无序以提高执行效率,重排序缓存把指令从无序重新变为有序以保证正确性,重命名寄存器则在乱序执行过程中临时存储数据。重命名寄存器与指令可以访问的结构寄存器(如通用寄存器、浮点寄存器)相对应。乱序执行流水线把指令执行结果写入重命名寄存器而不是结构寄存器,以避免破坏结构寄存器的内容,到顺序提交阶段再把重命名寄存器内容写入结构寄存器。两组执行不同运算但使用同一结构寄存器的指令可以使用不同的重命名寄存器,从而避免该结构寄存器成为串行化瓶颈,实现并行执行。

四、分支预测

4.1 为什么要分支预测

因分支指令而引起的控制相关也会造成流水线的阻塞。通过将分支指令处理放到译码阶段和分支指令延迟槽两项技术,可以在单发射5级静态流水线中无阻塞地解决控制相关所引起的冲突。但是这种解决控制相关所引起的冲突的方式并不是普适的。比如当为了提高处理器的主频而将取指阶段的流水级做进一步切分后,或者是采用多发射数据通路设计后,仅有1条延迟槽指令是无法消除流水线阻塞的。

正常的应用程序中分支指令出现十分频繁,通常平均每5~10条指令中就有一条是分支指令,而且多发射结构进一步加速了流水线遇到分支指令的频率。例如假设一个程序平均8条指令中有一条分支指令,那么在单发射情况下平均8拍才遇到1条分支指令,而4发射情况下平均2拍就会遇到1条分支指令。而且随着流水线越来越深,处理分支指令所需要的时钟周期数也越来越多。面对这些情况,如果还是只能通过阻塞流水线的方式来避免控制相关引起的冲突,将会极大地降低流水线处理器的性能。

现代处理器普遍采用硬件分支预测机制来解决分支指令引起的控制相关阻塞,其基本思路是在分支指令的取指或译码阶段预测出分支指令的方向和目标地址,并从预测的目标地址继续取指令执行,这样在猜对的情况下就不用阻塞流水线。既然是猜测,就有错误的可能。硬件分支预测的实现分为两个步骤:

  1. 第一步是预测,即在取指或译码阶段预测分支指令是否跳转以及分支的目标地址,并根据预测结果进行后续指令的取指;
  2. 第二步是确认,即在分支指令执行完毕后,比较最终确定的分支条件和分支目标与之前预测的结果是否相同,如果不同则需要取消预测后的指令执行,并从正确的目标重新取指执行。

4.2 分支预测对性能的影响

假设平均8条指令中有1条分支指令,某处理器采用4发射结构,在第10级流水计算出分支方向和目标(这意味着分支预测失败将产生(10-1)×4=36个空泡)。

  • 如果不进行分支预测而采用阻塞的方式,那么取指带宽浪费36/(36+8)=82%
  • 如果进行简单的分支预测,分支预测错误率为50%,那么平均每16条指令预测错误一次,指令带宽浪费36/(36+16)=75%
  • 如果使用误预测率为10%的分支预测器,那么平均每80条指令预测错误一次,指令带宽浪费降至36/(36+80)=31%
  • 如果使用误预测率为4%的分支预测器,则平均每200条指令预测错误一次,取指令带宽浪费进一步降至36/(36+200)=15%

从上文可以看出,在分支预测错误开销固定的情况下,提高分支预测的准确率有助于大幅度提升处理器性能。通过对大量应用程序中分支指令的行为进行分析后,人们发现它具有两个非常好的特性:首先,分支指令有较好的局部性,即少数分支指令的执行次数占所有分支指令执行次数中的绝大部分,这意味着只要对少量高频次执行的分支指令做出准确的预测就能获得绝大多数的性能提升;其次,绝大多数分支指令具有可预测性,即能够通过对分支指令的行为进行分析学习得出其规律性。

4.3 分支指令的特性

分支指令的可预测性主要包括单条分支指令的重复性以及不同分支指令之间存在的方向相关、路径相关。

单条分支指令的重复性主要与程序中的循环有关。例如for型循环中分支指令的模式为TT……TN(成功n次后跟1次不成功);while型循环中分支指令的模式为NN……NT(不成功n次后跟1次成功)。

不同分支指令之间的相关性主要出现在if…else…结构中。

  1. 下图(a)是分支指令之间存在方向相关的例子。两个分支的条件(完全或部分)基于相同或相关的信息,后面分支的结果基于前面分支的结果。
  2. 下图(b)是分支指令之间存在路径相关的例子。如果一个分支是通向当前分支的前n条分支之一,则称该分支处在当前分支的路径上,处在当前分支的路径上的分支与当前分支结果之间的相关性称为路径相关。

在这里插入图片描述

4.4 动态预测器

当一个分支指令第一次执行时,处理器在BTB上分配一个Entry并放入其中,以供后续再次执行到该指令是进行分支预测。

BTB是分支目标缓冲 (Branch Target Buffer)的简称,其逻辑上通常组织为一个基于内容寻址的查找表,每个表项包含 PC、 跳转目标 (Target)和饱和计数器(Counter)三个部分。 BTB 的预测过程是:用取指 PC 与表中各项的 PC 进行比较, 如果某项置相等且该项的饱和计数器值指示预测跳转, 则取出该项所存的跳转目标并跳转过去。

在这里插入图片描述
BTB的饱和计数器值指示分支跳转方向,他的值是通过什么方式确定的呢?由于单条分支指令的重复性,所以可单条分支指令的分支历史来预测分支指令的跳转方向, 对于重复性特征明显的分支指令 ( 如循环) 可以取得很好的预测效果。

例如, 对于循环语句 for( i = 0; i<10; i ++ ) { … } , 可以假设其对应的汇编代码中是由一条回跳的条件分支指令来控制循环的执行。 该分支指令前 9 次跳转, 第 10 次不跳转, 如果我们用 1 表示跳转, 0 表示不跳转, 那么这个分支指令的分支模式就记为 ( 1111111110) 。 这个分支模式的特点是, 如果上一次是跳转, 那么这一次也是跳转的概率比较大。 这个特点启发我们将该分支指令的执行历史记录下来用于猜测该分支指令是否跳转。 这种用于记录分支指令执行历史信息的表称为分支历史表 ( Branch History Table, 简称 BHT) 。

最简单的 BHT 利用 PC 的低位进行索引, 每项只有 1 位, 记录索引到该项的分支指令上一次执行时的跳转情况, 1 表示跳转, 0 表示不跳转。 由于存储的信息表征了分支的模式, 所以这种 BHT又被称为分支模式历史表(Pattern History Table, 简称 PHT))。

利用这种 1 位 PHT 进行预测时, 首先根据分支指令的 PC 低位去索引 PHT, 如果表项值为 1, 则预测跳转, 否则预测不跳转;其次要根据该分支指令实际的跳转情况来更新对应 PHT 的表项中的值。 仍以前面的 for 循环为例, 假设 PHT 的表项初始值都为 0:

  1. 分支指令第 1 次执行时, 读出的表项为 0 所以预测不跳转, 但这是一次错误的 预测, 第 1 次执行结束时会根据实际是跳转的结果将对应的表项值更新为 1;
  2. 分支指令第 2 次执行时, 从表项中读出 1 所以预测跳转, 这是一次正确的预测, 第 2 次执行结束时会根据实际是跳转的结果将对应的表项值更新为 1;
  3. …;
  4. 分支指令第 10 次执行时, 从表项中读出 1 所以预测跳转, 这是一次错误的预测, 第 10 次执行结束时会根据实际是不跳转的结果将对应的表项值更新为 0。

可以看到进入和退出循环都要猜错一次。 这种 PHT 在应对不会多次执行的单层循环时, 或者循环次数特别多的循环时还比较有效。 但是对于如下的两重循环:

for(i =0;i<10;i++)
	for(j =0;j<10;j++){
		...
	}

使用上述 1 位 PHT, 则内外循环每次执行都会猜错 2 次, 这样总的分支预测正确率仅有 80%。

为了提高上述情况下的分支预测正确率, 可以采用每项 2 位的 PHT。 这种 PHT 中每一项都是一个 2 位饱和计数器, 相应的分支指令每次执行跳转就加 1 ( 加到 3 为止) , 不跳转就减 1 ( 减到 0 为止) 。 预测时, 如果相应的 PHT 表项的高位为 1 ( 计数器的值为 2 或 3) 就预测跳转, 高位为 0 ( 计数器的值为 0 或 1) 就预测不跳转。 也就是说, 只有连续两次猜错, 才会改变预测的方向。 使用上述 2 位 PHT 后, 前面两重循环的例子中, 内层循环的预测正确率从 80%提高到 (7 + 81) / 100 = 88%

图9.22给出了 2 位 PHT 分支预测机制的示意。

在这里插入图片描述
还有很多技术可以提高分支预测的准确率。可以使用分支历史信息与PC进行哈希操作后再查预测表,让分支历史影响预测结果;可以使用多个预测器同时进行预测,并预测哪个预测器的结果更准确,这被称为锦标赛预测器。

五、高速缓存

由于物理实现上存在差异,自20世纪80年代以来CPU和内存的速度提升幅度一直存在差距,而且这种差距随着时间的推移越来越大。例如DDR3内存的访问延迟约为50ns,而高端处理器的时钟周期都在1ns以下,相当于每访问一次DDR3都需要花费至少50个处理器的时钟周期,如果程序有较多依赖访存结果的数据相关,就会严重影响处理器的性能。处理器和内存的速度差距造就了存储器层次结构

Cache为了追求访问速度,容量通常较小,其中存放的内容只是主存储器内容的一个子集。Cache是微体系结构的概念,它没有程序上的意义,没有独立的编址空间,处理器访问Cache和访问存储器使用的是相同的地址,因而Cache对于编程功能正确性而言是透明的。Cache在流水线中的位置大致如下图所示,这里为了避免共享Cache引入的结构相关采用了独立的指令Cache和数据Cache,前者仅供取指,后者仅供访存。

在这里插入图片描述
由于Cache没有独立的编址空间,且只能存放一部分内存的内容,所以一个Cache单元可能在不同时刻存储不同的内存单元的内容。这就需要一种机制来标明某个Cache单元当前存储的是哪个内存单元的内容。因此Cache的每一个单元不仅要存储数据,还要存储该数据对应的内存地址(称为Cache标签,Tag)以及在Cache中的状态(如是否有效,是否被改写等)。关于Cache更详细的介绍,在《CSAPP:第六章——存储器层次结构》的高速缓存存储器中总结过。

【参考书籍】
计算机体系结构基础》第3版
深入理解计算机系统》第3版
计算机体系结构量化研究方法》第5版

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值