3.1 指令级并行:概念和挑战
1985年之后几乎所有处理器都使用流水线来使指令能重叠执行。由于指令可以并行执行,所有指令之间的这种可能得重叠称为指令级并行ILP。
ILP大体有两种实现方法:
1. 依靠硬件来动态发现并实现并行;
2. 依靠软件技术在编译时静态发现并行;
基于硬件的动态方法的处理器,在桌面端和服务器、个人移动设备占据主导地位。在物联网领域,功耗和成本约束性能目标,,设计者只实现了较低水平的指令级并行。从20世纪80年代开始,尽管对基于编译器的方法付出巨大努力,但这些方法仅在特定领域的场景中包含大量数据级并行的、结构良好的科学应用程序中取得了成功。
过去几年里,针对其中一种方法开发的许多技术,已经在主要依赖于另一种方法的设计中得到了应用。ILP方法的局限性直接导致了向多核的演变。
限制指令间并行度的程序和处理器的特性,以及程序结构和硬件结构之间的关键限制,后者对于理解程序属性是否会限制性能以及什么情况下会限制性能非常关键。
一个流水线化处理器的CPI(每条指令占用的周期数)值等于基本CPI与因为各种停顿而耗费的全部周期之和:
流水线CPI = 理想流水线CPI+结构化停顿+数据冒险停顿+控制停顿;
理想流水线CPI是对能够实现的最佳性能的度量。
技术 | 降低CPI的那一部分 |
前递和旁路 | 潜在的数据冒险 |
简单分支预测和预测 | 控制冒险 |
基本编译器流水线调度 | 数据冒险 |
基本动态调度 | 由真依赖引起的数据冒险 |
循环展开 | 控制冒险 |
高级分支预测 | 控制冒险 |
采用重命名的动态调度 | 数据冒险 |
硬件推测 | 数据冒险和控制冒险 |
动态存储器消除二义性 | 涉及存储器的数据冒险 |
每个周期多指令发射 | 理想CPI |
编译器依赖分析、软件流水线、跟踪调度 | 理想CPI、数据冒险 |
编译器推测的硬件支持 | 理想CPI、数据冒险、控制冒险 |
3.1.1 什么是指令级并行
基本快(一个顺序代码序列,除入口外没有其他转入分支,除出口外没有其他转出分支)中可用的并行度非常小。由于基本块内指令可能的相互依赖,所以在基本块中可以利用的重叠量可能要小于基本块的平均大小。为了获得实质性的性能增强,必须跨越多个基本块利用ILP。
提高ILP的最简单、最常见的方法是在一个循环的各次迭代之间利用并行,通常被称为循环级并行。
研究一些将这种歌循环级并行转换为指令级并行的技术,基本上是利用编译器静态展开循环或者利用硬件动态展开循环。
利用循环级并行的一种重要替代方法是使用向量处理器和图形处理器中的SIMD。SIMD指令在利用数据级并行时,并行处理少量到中等数量的数据项(2~8)。而向量指令在利用数据级并行时,则通过使用并行执行单元和深流水线,并行处理器许多数据项。
3.1.2 数据依赖与冒险
对于确定一个程序中的并行度以及如何利用并行,判断指令之间的依赖关系至关重要。
1. 数据依赖:
数据依赖、名称依赖和控制依赖。如果一下任意一个条件成立,则说指令j数据依赖于指令i。
1. 指令i生成的结果可能会被指令j用到;
2. 指令j数据依赖于指令k,指令k数据依赖于指令i;
单条指令内部的依赖不视为依赖。
如果两条指令是数据依赖的,那它们必须按顺序执行,不能同时执行,也不能完全重叠执行。这种依赖意味着两条指令之间存在由一个或多个数据冒险构成的链。同时执行这些指令会导致一个具有流水线互锁的处理器检测到冒险并停顿,从而减少或消除重叠。
依赖是程序的一种属性。某种给定依赖是否会导致检测到实际冒险,这一冒险又是否会实际导致停顿,则都属于流水线结构的性质。这一区别对于理解如何利用指令级并行至关重要。
数据传递传递了3点信息:
1. 冒险的可能性;
2. 计算结果时必须遵循的顺序;
3. 可利用并行度的上限;
克服数据依赖的方法:
1. 保持依赖单避免冒险;
2. 通过转换代码来消除依赖;
对代码进行调度是在不改变依赖的情况下避免冒险的主要办法,这种调度既可以由编译器完成,也可以由硬件完成。
数据值既可以通过寄存器也可以通过存储地址在指令之间传送。当通过寄存器传送数据时,由于指令中的寄存器名称是固定的,所以依赖关系容易检测。而通过存储地址传送时,依赖关系难以检测。
本章研究采用硬件来检测那些涉及存储地址的数据依赖。用于检测这些依赖关系的编译器技术是揭示循环级并行的关键。
2. 名称依赖
当两条指令使用相同的寄存器或存储地址,但与该名称相关的指令之间并没有数据流动时,就会发生名称依赖。在指令i和j之间(i在j前执行)存在2种类型的名称依赖:
1. 当指令j对指令i读取的寄存器或存储地址执行写操作数时,就会在指令i和指令j之间发生反依赖。
2. 当指令i和j对同一个寄存器或存储器地址执行写操作时,发生输出依赖。
因为没有在指令之间传递值,所以名称依赖不是真正的依赖。改变这些指令中使用的名称(寄存器、存储地址),使这些指令不再冲突,那么名称依赖中涉及的指令就可以重新排序或同时执行。
寄存器重命名即可以由编译器静态完成,也可以由硬件动态完成。
3. 数据冒险
只要指令间存在名称依赖或数据依赖,而且它们非常接近,以至于执行期间的重叠改变对应依赖中操作数的访问顺序,就会存在冒险。
软、硬件技术的目的都是通过只在影响程序输出的地方保持程序顺序来利用并行性。检测冒险和避免冒险可以确保不会打乱必要的程序顺序。
根据指令中读访问和写访问的顺序,可以将数据冒险分为3类:
1. 写后读--真依赖;
2. 写后写--输出依赖;
3. 读后写--反依赖;
3.1.3 控制依赖
控制依赖决定了指令i相对于分支指令的顺序。除了程序中第一基本块中的指令之外,其他所有指令都与某组分支存在控制依赖。一般来说,为了保持程序顺序,必须保留这些控制依赖。
控制依赖会施加以下两条约束:
1. 如果一条指令控制依赖于一个分支,那就不能把这个指令移到这个分支之前,使它的执行不再受控于这个分支。
2. 如果一条指令并不控制依赖于一个分支,那就不能把这个指令移到这个分支之后,使其执行受控于这个分支。
对程序正确性至关重要的两个特性是异常行为和数据流,通常保持数据依赖和控制依赖也就保护了这2种特性。
保护异常行为意味着任何指令执行顺序的改变都不得改变程序抛出异常的方式。
通过维护数据依赖和控制依赖来保护的第二个特性是数据流。数据流是指数据值在生成结果和使用结果的指令之间的实际流动。
由于一条指令可能会与之前的多条指令存在数据依赖,所以仅保持数据依赖是不够的。一条指令的数据值究竟是由之前的那条指令提供,是由程序顺序决定的。而程序顺序是通过维护控制依赖来保持的。
推测不但可以帮助解决异常问题,还能在保持数据流的同时减轻控制依赖的影响。
在寄存器活性能够确定的情形下,可以断定破坏控制依赖并不会影响异常行为或数据流。
编译器对分支结果进行猜测,即软件推测。
3.2 利用ILP的基本编译器技术
为使流水线保持满载,必须找出可以在流水线中重叠的不相关指令序列,以充分利用指令并行。为了避免流水线停顿,必须将依赖指令与源指令的执行隔开一定的时间周期,这一间隔应等于源指令的流水线延迟。编译器执行这种调度的能力即依赖于程序中可用的ILP,也依赖于流水线中功能单元的延迟。
相对于分支和开销指令而言,增加指令数量的一个简单方法是循环展开。循环展开还可以用于提高调度效率。由于它消除了分支,因此可以将来自不同迭代的指令放在一起调度。如果在展开循环时只是简单地复制这些指令,最后使用的就是同一组寄存器,所以可能会妨碍对循环的有效调度。因此,我们希望在每次迭代中使用不同的寄存器,这就增加了所需的寄存器的数量。
假定此上限为n,生成循环体的k个副本。我们生成的是一对连续,而不是单个展开后的循环,第一个循环执行n mod k次,其主体就是原来的循环。第二个循环是由外层循环包围的展开循环,迭代n/k次。
对展开循环进行调度的收益大于对原循环进行调度的收益,因为展开后的循环暴露了更多的进行调度的计算,从而可以将停顿时间减少至最低。
3.2.2 循环展开与调度小结
大多数利用指令级并行的硬件和软件技术的关键在于判断何时能够改变指令顺序以及如何改变。
为了获得最终展开后的代码,必须进行以下决策和变换:
1. 找出除维护循环的代码外互不相关的循环迭代,判定循环展开是否有用的;
2. 使用不同寄存器,以避免由于不同运算使用相同寄存器而造成的非必要约束(比如,名称依赖);
3. 去除多余的测试和分支指令,并调整循环终止与迭代代码;
4. 通过观察不同迭代中的载入指令和存储指令的互不相关,判定展开过后的循环中的载入指令和存储指令可以交换位置。这一变换需要分析存储器地址,确认它们没有引用同一地址;
5. 在保留必要的依赖,以得到与源代码相同结果的前提下,对代码进行调度。
有3种效果会限制循环展开带来的好处:
1. 每次展开操作分摊的开销降低;
2. 代码规模限制;
3. 编译器限制;
随着展开变多,循环中索引变量和分支判断下降的收益在递减。对于较大规模的循环,代码规模的增长可能会导致指令缓存缺失率上升。以及由于大量进行展开和调度造成寄存器数量不足。这是因为调度代码以增加ILP时导致存活值的数量增加。
循环展开能够增大有效调度的直线代码片段的规模。
3.3 用高级分支预测降低分支成本
由于需要通过分支冒险和停顿来实现控制依赖,所以分支会有损流水线性能。
因为运行中的指令数量随着更深流水线和每个时钟更多的发射而增加,更准确的分支预测也变得越来越重要。
3.3.1 相关分支预测器
如果我们不仅仅考察预测分支的历史信息,还查看其他分支最近的行为,就有可能提高预测准确率。
利用其他分支的行为来进行预测分支的分支预测器称为相关预测器或2级预测器。(m,n)预测器利用最近m个分支的行为在2^m个分支预测器中进行选择,其中每个分支预测器都是单个分支的n位预测器,该方法的优点在于预测率高于2位预测器,且需要添加的硬件很少。
3.4 用动态调度克服数据冒险
除非是流水线中的已有指令与要读取的指令之间存在数据依赖,而且无法通过旁路或前递来隐藏这一数据依赖,否则,简单的静态调度流水线就会提取一条指令发射出去。如果不能隐藏的数据依赖,那么冒险检测硬件会从该结果的指令开始,将流水线置于停顿状态,在清楚这一依赖之前,不会提取和发射新的指令。
在动态调度方式中,硬件会重新安排指令的执行顺序以减少停顿,同时保持数据流和异常行为。动态调度的优点在于:
1. 它允许针对一种流水线编译的代码在不同类型的流水线上高效执行,不需要多个二进制文件,也无需位不同的微体系结构重新进行编译。
2. 它可以应对编译时依赖关系未知的情况;比如设计存储器访问或分支,或者源自于动态链接库或动态分发的现代编程环境。
3. 它允许处理器容忍一些预料之外的延迟、比如缓存缺失。
动态调度的优势以显著增加硬件复杂度为代价。
尽管动态调度的处理器不能改变数据流,但它会在存在依赖关系时尽量避免停顿。相反,编译器静态调度尽量将停顿时间降至最低,具体方法是隔离相关操作,使它们不会导致冒险。
3.4.1 动态调度:思想
简单流水线技术的一个主要限制是:它们使用顺序指令发射与执行。如果一条指令停顿在流水线中,后续指令都不能执行。
在经典的五级流水线中,可在译码ID期间检查结构冒险和数据冒险:当一个指令可以无冒险执行时,它会从ID发射出去。
为了解决数据冒险,必须将发射过程分为2个部分:检查所有结构冒险和等待数据冒险的消失。我们仍然可以使用顺序指令发射,但希望一条指令能够在其操作数可用时立即开始执行。这样的流水线实际是乱序执行。
乱序执行可能导致WAR冒险和WAW冒险,而这些冒险在这个五级流水线及其逻辑扩展中的顺序流水线中是不存在的,可以用寄存器重命名来避免这2种冒险。
乱序完成还会使异常处理变得复杂。动态调度的处理器会通过推迟相关异常的发布来保留异常行为,直到处理器直到该指令就是接下来要完成的指令为止。但动态调度的处理可能会造成非精确异常,即在发生异常时,处理器的状态与按序执行时的状态不完全一致。可能原因如下:
1. 流水线在执行导致异常的指令时,可能已经完成了按照程序排在这一指令之后的指令;
2. 流水线在执行导致异常的指令时,可能还没有完成按照程序排在这一指令之前的指令;
为了能够进行乱序执行,将五级简单流水线的ID大体分为以下2个阶段:
1. 发射--指令译码,检查结构冒险;
2. 读取操作数--直到没有数据冒险后,读取操作数;
我们区分一个指令开始执行和完成执行的时间,在这两个阶段之间,指令处于执行过程中,我们的流水线允许同时执行多条指令,如果没有这一功能,就会失去动态调度的主要优势。要同时执行多条指令,需要有流水化单元或多个功能单元,或者二者兼有。
支持乱序执行的2种方法:
1.记分牌技术允许在有足够资源且不存在数据依赖时乱序执行指令。
2.Tomasulo算法,它们之间主要区别在于,Tomasulo算法通过对寄存器进行有效的动态重命名来处理反依赖和输出依赖。此外,还可以对Tomasulo算法进行扩展,用来处理推测,这种技术通过预测一个分支的输出、执行预测目标地址的指令、在预测错误时采取纠正措施,降低控制依赖的影响。
3.4.2 使用Tomasulo算法进行调度
该算法是在IBM30浮点单元开发过程中,由Robert Tomasulo发明的。该算法会跟踪之类的操作数何时可用,以将RAW冒险降至最低,还会在硬件中引入寄存器重命名功能,以将WAW冒险和WAR冒险降至最低。依赖的2个关键原则是:
1. 动态确定一条指令何时可以执行;
2. 重命名寄存器以避免不必要的冒险;
IBM360目标是为整个360计算机系列设计的指令集和编译器,而不是针对高端处理器的专门编译器来实现高浮点性能。380体系结构只有4个双精度浮点寄存器,这限制了编译器调度的有效性;360的存储器访问时间和浮点延迟都很长,该算法可以克服此问题。
在Tomasulo方案中,寄存器重命名功能由保留站提供,保留站会为等待发射的指令缓冲操作数,并且与功能单元无关。其基本思想是:保留站在一个操作数可用时马上提取并缓冲它,这样就不再需要从寄存器中获取该操作数。此外,等待执行的指令会指定保留站为自己提供输入。最后,在对寄存器连续进行写操作并且重叠执行时,实际只会使用最后一个操作更新寄存器。在发射指令时,会重命名待用操作数的寄存器说明符,改为提供寄存器重命名功能的保留站的名字。
由于保留站的数目可能多于实际的寄存器,所以这一技术甚至可以消除因为名称依赖而导致的冒险,这类冒险是编译器所无法消除的。
使用保留站而不是集中式寄存器堆,还有另外2个重要特性:
1. 冒险控制和执行控制是分布式的:每个功能单元保留站中保存的信息,决定了一条指令什么时候开始在该单元中执行。
2. 结果将直接从缓冲它们的保留站传递给功能单元,而不需要经过寄存器。在具有多个执行单元并且每个时钟周期发射多条指令的流水线中,将需要不止一条结果总线。
先看一条指令所经历的步骤:
(1)发射:从指令队列的头部获取下一条指令。如果没有空闲保留站,则存在结构冒险,该指令会停顿,直到有保留站或缓冲区被释放为止。如果操作数不在寄存器中,则一直跟踪将生成这些操作数的功能单元。这一步骤称为对寄存器进行重命名,消除WAR冒险和WAW冒险。在动态调度处理器中,这一阶段有时被称为分派dispatch。
(2)执行:如果还有一个或多个操作数不可用,则在等待计算的同时监视公共数据总线。当一个操作数可用时,就将它放到任何一个正在等待它的保留站中。当所有操作数都可以用时,则可以在相应的功能单元中执行运算。通过延迟指令执行,直到操作数可用为止,可以避免RAW冒险。注意,同一功能单元可能会同时拥有几条就绪指令,那么功能单元必须从这些指令中选择。对于浮点保留站,可以任意做出这一选择,但是载入和存储指令可能要更复杂一些。
载入和存储指令的执行过程分为两步,第一步是在基址寄存器可用时计算有效地址,然后将有效地址放入缓冲区中。载入缓冲区的载入指令在存储器单元可用时立即执行,而存储缓冲区的存储指令要等待存储的值,然后将其发射给存储器单元。
为了保护异常行为,任何一条指令必须等到根据程序顺序排在它之前的所有分支全部完成之后,才能执行。这一限制保证了在执行期间导致异常的指令确实会被执行。
(3)写结果
在计算结果之后,将其写到CDB上,再从CDB传送给寄存器和所有等待这一结果的保留站(包括存储缓冲区)。存储指令一直缓存在存储缓冲区中,直到代存储值和存储地址可用为止,然后在有空闲存储器单元时,立即写入结果。
在指令被发射出去并等待源操作数之后,将使用一个保留站编号来引用该操作数,这个保留站中保存着对寄存器进行写操作的指令。如果使用一个未用作保留站的值来引用该操作数,则表明该操作数已经在寄存器中准备就绪。由于保留站的数目多余实际寄存器的数目,所以使用保留站编号对结果进行重命名,就可以避免WAW冒险和WAR冒险。
在Tomasulo方法以及后面将介绍的支持推测的方法中,结果都是在受保留站监视的总线上广播。采用公共结果总线,再由保留站从总线中提取结果,就实现了静态调度流水线中的前递和旁路机制。但在动态调度方法中(Tomasulo算法)会在源和结果之间引入一个时钟周期的延迟,这是因为相对于一个较简单的流水线“执行”阶段的末尾,要等到“写结果”阶段的末尾,才能让其能够结果与其应用匹配起来。因此,在动态调度流水线中,生成结果的指令与使用结果的指令之间的有效延迟,要比生成该结果的功能单元的延迟至少长一个时钟周期。
Tomasulo方法中的标签引用的是将生成结果的缓冲区或单元;当一条指令发射到保留站之后,寄存器名称将被丢弃掉,这与记分牌完全不同。在记分牌中,操作数保存在寄存器中,只有生成结果的指令已经完成,使用结果的指令做好执行的准备之后,才会读取读取操作数。
每个保留站有以下7个字段:
Op--执行运算的类型;
Qj、Qk--将生成相应源操作数的保留站;当取指为0时,表明可以在Vj、Vk中获取,或者不需要源操作数;
Vj、Vk--源操作数的值。注意对于每个操作数,V字段或Q字段只有1个是有效的。对于载入指令,Vk字段用于保存偏移量字段;
A--用于保存为载入指令或存储指令而计算存储器地址所需的信息。在开始时,指令的立即数字段存储在这里;在计算地址之后,有效地址存储在这里;
Busy--指明保留站及其功能单元已被占用;
Qi--如果一个运算的结果应当存储在这个寄存器中,则Qi是包含次运算的保留站的编号。如果Qi为空或0,则当前没有活动指令正在计算应当存储在这个寄存器中的结果。
在360/91之后的许多年,Tomasulo方案一直没有得到应用,但从20世纪90年代开始,多发射处理器广泛采用Tomasulo方案,原因如下:
(1)尽管Tomasulo方案是在缓存出现之前设计的,但缓存的出现及其固有的不可预测的延迟,已经成为使用动态调度的主要动力之一。乱序执行可以让处理器在等待缓存缺失的同时继续执行指令,从而消除了全部或部分缺失代价。
(2)随着处理器的发射功能变得越来越强大,设计人员越来越关注难以调度的代码(比如,大多数非数值代码)的性能,所以诸如寄存器重命名、动态调度和推测等技术变得越来越重要。
(3)无需编译器针对特定流水线来编译代码,Tomasulo方案就能实现高性能。在标准的大众市场软件的时代,这是一个非常富有价值的性质。
3.6 基于硬件的推测
当我们尝试利用更多指令级并行时,维护控制依赖就会成为一项不断加重的负担。分支预测减少了分支导致的直接停顿,但对于每个时钟周期要执行多条指令的处理器来说,仅靠正确地预测分支不足以生成期望数量的指令级并行。
通过预测分支的输出,然后在假定猜测正确的前提下执行程序,可以克服控制依赖问题。这种机制是对采用动态调度的分支预测的虽细微但很重要的扩展。具体而言,通过推测,我们提取、发射和执行指令,就好像分支预测总是正确的:而动态扩展知识提取和发射这些指令。
基于硬件的推测结合了3种关键思想:
(1)用动态分支预测选择要执行那些指令;
(2)利用推测,可以在解决控制依赖问题之前执行指令(能撤销错误推测指令序列的影响);
(3)进行动态调度,以应对不同组合方式的基本模块调度;
为了扩展Tomasulo算法以支持推测,我们必须将指令结果的旁路从一条指令的实际完成操作中分离出来。然后允许执行一条指令,并将其结果旁路给其他指令,但不允许这条指令执行任何不能撤销的更新操作,直到确认这条指令不再具有不确定性为止。
使用旁路的值类似于一次推测寄存器读操作,因为提供源寄存器的指令还未提交退休。
实现推测背后的关键思想是允许指令乱序提交,但强制它们顺序提交,并防止在指令提交之前采取任何不可撤销的动作(比如更新状态或引发异常)。因此,当我们添加推测时,需要将执行完成的过程与指令提交分隔开来。相应新增硬件为重排序缓冲区ROB,用于保存已经完成执行但还没有提交的指令结果。此硬件也可以用于在可被推测的指令之间传送结果。
ROB类似于Tomasulo算法通过保留站扩展寄存器集一样,二者关键区别:在Tomasulo算法中,一旦一条指令写出其结果,任何后续发射的指令都会在寄存器堆中找到该结果。而在采用推测时,寄存器堆要等到指令提交之后才会更新;因此,ROB是在指令执行完毕到指令提交这段时间内提供操作数。
ROB中每个项目包含4个字段:
(1)指令类型:指定这个指令类型,如分支(没有目的地结果)、访存(含有存储器目的地)、运算等;
(2)目的地字段:提供了应当向其中写入指令结果的寄存器编号(载入和ALU运算)或存储器地址(存储指令)
(3)值字段:提交指令之前保存指令结果;
(4)就绪字段:表示指令已经完成执行,结果值准备就绪。
尽管保留站重命名功能由ROB代替,但在发射运算之后仍然需要一个空间来缓冲它们(以及操作数),直到它们开始执行为止。这一功能仍然由保留站提供。由于每条指令在提交之前都在ROB拥有一个位置,所以我们使用ROB条目编号而不是保留站编号来标记结果。
在指令执行时涉及以下4个步骤:
(1)发射--从指令队列获得一条指令。如果存在空闲保留站而且ROB中有空插槽,则发射该指令;如果寄存器或ROB中已经含有这些操作数,则将其发送给保留站。更新控制项,指明这些缓冲区正在使用中。为指令结果分配的ROB条目编号也被发送到保留站,以便在结果放在CBD上时,可以使用这个编号来标记结果。如果所有保留站都被占满或者被ROB被占满,则指令发射过程停顿,直到这两者都有可用条目为止。
(2)执行--如果还有一个或多个操作数不可用,则在等待寄存器值被计算的同时监视CDB。这一步骤检查RAW冒险。当保留站中拥有这两个操作数时,执行该运算。指令在这一阶段可能占用多个时钟周期,而载入操作在这阶段仍然需要两个步骤。此时执行存储指令只是为了计算有效地址,所以在这一阶段只需要有基址寄存器可用即可。
(3)写结果--当结果可用时,将它写在CDB上(还有在发射指令时发送的ROB标签),并从CDB写到ROB以及任何等待这一结果的保留站。将保留站标记为可用。对于存储指令需要执行一些特殊操作。如果要存储的值已经准备就绪,则将它写到ROB条目的值字段,已备存储。如果要存储的值还不可用,则必须监视CDB,直到该数值被广播,在更新该存储指令ROB条目的值字段。
(4)提交--根据要提交的指令是预测错误的分支指令、存储指令,还是任意其他指令(正常提交),在提交时共有3种操作序列。当一个指令到达ROB的头部头部而且其结果出现在缓冲区时,进行正常提交;此时,处理器用结果更新其寄存器,并从ROB中清除该指令。提交缓存指令与正常提交类似,但更新的是存储器而不是结果寄存器。当预测错误的分支指令到达ROB头部时,它指出推测是错误的。ROB被清空,执行过程从该分支的正确后续指令重新开始。如果对该分支的预测正确,则该分支完成提交。
指令一旦提交完毕,它在ROB的相应项将被回收,寄存器或存储器目的地址将被更新,并且不再需要ROB项。如果ROB填满,则停止发射指令,直到有空闲条目为止。
具有ROB的处理器可以在维持精确中断模式的同时,动态执行代码。
在实践中,进行推测的处理器会在错误预测一个分支后尽早恢复。将预测错误的分支之后的所有ROB条目清空,使该分支之前的ROB条目继续执行,并在后续的正确分支处重新开始取指,从而完成恢复操作。
在处理异常时,要等到做好提交提交准备时才会识别异常。如果推测的结果引发异常,则将异常记录在ROB中国。如果分支预测错误,则在清除ROB时,异常将于指令一起被清除。如果指令到达ROB的头部,就知道它不再具有不确定性,应当引发该异常。也可以在异常出现之后、所有先前分支都已处理完毕的情况下,立即处理异常,但异常比分支预测错误更难处理。
在处理存储指令时,支持推测执行的处理器与Tomasulo算法有一个重要的区别。在Tomasulo算法中,一条存储指令可以在到达“写结果”阶段(确保已经计算出有效地址)且待存储值可用时,就更新存储器。在支持推测执行的处理器中,只有当存储指令到达ROB的头部时才能更新存储器。这一区别保证当指令不再具有不确定性时才会更新存储器。
和Tomasulo算法一样,我们必须避免存储器冒险。同推测可以消除存储器中的WAW冒险和WAR冒险,这是因为存储器更新时顺序进行的,当存储指令位于ROB头部时,先前不可能再有未完成的载入或存储指令。通过以下两点限制来解决存储器中的RAW冒险:
(1)如果一条指令被存储指令占用的活动ROB条目“字段”与一条载入指令的A字段取值匹配,则不允许该载入指令开始执行第二步骤。
(2)在计算一条载入指令的有效地址时,保持相对于所有先前存储指令的程序顺序。
这两条限制条件共同保证了:对于任何一条载入指令,如果它要访问由先前存储指令写入的存储地址,俺么在这条存储指令写入数据之前,它不能执行存储器访问。
只要允许在每个时钟周期内发射和提交多条指令,就可以将这些技术扩展到多发射处理器中工作。事实上,推测可能是这类处理器中最有趣的部分,因为在编译器的帮助下,不太复杂的技术也有可能在基本模块中利用足够的指令级并行。
3.7 以多发射和静态调度来利用ILP
为了进一步提高性能,希望CPI小于1,但如果每个时钟周期仅发射一条指令,那CPI是不可能降低小于1的。
多发射处理器的目标就是允许在一个时钟周期中发射多条指令。多发射处理器主要有以下3种:
(1)静态调度超标量处理器
(2)超长指令字VLIW处理器
(3)动态调度超标量处理器
两种超标量处理器每个时钟发射不同数目的指令,如果它们采用静态调度则顺序执行,如果采用动态调度则乱序执行,
与之相对,VLIW处理器每个时钟周期发射固定数目的指令,这些指令可以设置为两种方式:
(1)长指令:一个固定的指令包,指令之间的并行度由指令显式地表示出来。VLIW处理器由编译器进行静态调度。
尽管静态调度的超标量处理器在每个周期内发射的指令数是可变的,而不是固定的,但它在概念上与VLIW更接近一点,这因为二者都是靠编译器为处理器调度代码。由于静态调度超标量的收益会随着发射宽度的增长而逐渐减少,所以静态调度的超标量主要用于发射宽度较窄的情况,通常只有两条指令。超过这一宽度之后,大多数设计人员选择实现VLIW或动态调度超标量。
常见名称 | 发射结构 | 冒险检测 | 调度 | 显著特征 |
静态超标量 | 动态 | 硬件 | 静态 | 顺序执行 |
动态超标量 | 动态 | 硬件 | 动态 | 一些乱序执行,但没有推测 |
推测超标量 | 动态 | 硬件 | 带有推测的动态 | 具有推测的乱序执行 |
VLIW | 静态 | 以软件为主 | 静态 | 所有冒险由编译器判断和指出 |
EPIC | 以静态为主 | 以软件为主 | 大多为静态 | 所有冒险由编译器隐式判断指出 |
基于VLIW方法
VLIW使用多个独立的功能单元。VLIW没有尝试向这些单元发射多条独立指令,而是将多个操作包装在一个非常长的指令中,或者要求发射包中的指令满足同样的约束调价。
由于VLIW的收益会随着最大发射率的增长而增长,所以我们关注宽发射处理器。
超标量的开销的增长是限制宽发射处理器的主要因素。
为使功能单元保持忙碌状态,代码序列必须具有足够的并行度,以填充可用的操作槽。这种并行是通过循环展开和调度单个较大循环体中的代码实现的。如果展开过程会生成直行代码,则可以使用局部调度技术,它可以对单个基本模块进行操作。如果并行的发现和利用需要再分支之间调度代码,那就必须使用更为复杂的全局调度算法。全局调度算法不仅在结构上更为复杂,而且由于在分支之间移动代码的成本很高,所以它们还必须进行非常复杂的优化权衡。
原始VLIW模块中即存在技术问题也存在逻辑问题,从而降低了该方法的效率。技术问题包括代码大小的增加和锁步操作的局限性。造成VLIW代码体量的增大原因有二:
(1)要在直行代码中生成足够多的操作,需要大量展开循环,从而增大了代码大小。
(2)只要指令未被填满,那么没有用到的功能单元就会在指令编码时变为多余的位。
为了应对代码大小的增长,有时会使用灵活的编码。比如一些条例可能只有很大的立即数供所有功能单元使用。另外一种技术是在主存储器中压缩指令,然后在读入缓存或进行译码时再展开它们。
早期的VLIW是锁步工作的,根本没有冒险检测硬件。导致所有功能单元流水线中的停顿都必然导致整个处理器停顿。对于想预测那些数据访问会遭遇缓存停顿,并对他们进行调度,是非常困难的。因此,缓存需要被阻塞,并导致所有功能单元停顿。随着发射速度和存储器访问次数不断变大,这一同步限制变得不可接受。在最近的处理器中,这些功能以更独立的方式工作,并且在发射时利用编译器来避免冒险,而在发射指令之后,可以通过硬件检测来进行异步执行。
二进制代码的兼容性也是通用VLIW或运行第三方软件的VLIW的一个主要逻辑问题。在严格的VLIW方法中,代码序列即利用指令集定义,又要利用具体的流水线结构,包括功能单元及其延迟。因此,当功能单元数目和单元延迟不同时,就需要不同的代码版本,这个需求使得在演进的实现之间,或者在具有发射宽度的实现之间移植代码,比在超标量设计中更加困难。
EPIC方法为早期通用VLIW设计中的许多问题提供了解决方案,包括用于更加先进的软件推测的扩展,以及在保证二进制兼容性的前提下克服硬件依赖限制的方法。
所有多发射处理器都要面对的重要挑战是尝试利用大量ILP。对于简单循环而言,多发射处理器是否优于向量处理器尚不清楚;其成本是类似的,向量处理器的速度可能比多发射处理器更快。多发射的潜在优点在于它们能够从结构化程度较低的代码中提取一些并行性,并且能够轻松地缓存所有形式的数据。
3.8 以动态调度、多发射和推测来利用ILP
在动态调度处理器中,每个时钟周期发射多条指令是非常复杂的,原因很简单,即这些指令之间可能相互依赖。因此,必须为这些并行指令更新控制表,否则会丢失依赖。
在动态调度的处理器中,多发射方法基于以下事实:要在每个时钟周期内发射多条指令,关键是分配一个保留站和更新流水线控制表。方法有二:
(1)在半个时钟周期内运行这一步骤,以便在一个时钟周期内运行2条指令,遗憾的是,很难将这一方法扩展到每个时钟周期处理4条指令。
(2)构建必要逻辑,一次处理2条或更多条,包括指令之间可能存在的依赖关系。
这两种方法可能都采用流水线方式,也拓宽发射逻辑。一个重要的事实是:仅靠流水线无法解决这一问题。由于每个时钟周期都会发射新指令,所以通过让指令发射占用多个时钟周期,必须能够分配保留站,并更新流水线表,使下一个时钟周期的相关指令能够录用更新后的信息。
在动态调度的超标量中,发射步骤是最基本的瓶颈之一。
在现代超标量中,必须考虑可以在同一时钟周期内发射的相关指令的所有可能的组合。这种组合数与一个周期内可发射指令数的平方成正比。
描述动态调度的超标量中更新发射逻辑和保留表的基本策略,如下所示:
(1)为可能在下一个发射包中发射的每条指令指定保留站和重排序缓冲区。这可以在指导指令类型之前完成,只需使用n个可用的重排序缓冲区项,将重排序缓冲区项按顺序预先分配给发射包中的指令,并确保有足够的保留站可用于发射整个包即可。通过限制给定类别的指令数目,就要可以预先分配必要的保留站。如果没有足够的保留站可用,则将这个包分解,仅根据原始程序顺序,发射其中一部分指令。包中的其余指令可以放在下一个发射包中,为潜在的发射做准备。
(2)分析发射包中指令之间的所有依赖关系。
(3)如果包中的一条指令依赖于包中先前的一个指令,则使用指定的重排序缓冲区编号来更新相关指令的保留表。否则,使用已有的保留表和重排序缓冲区信息更新发射指令的保留表项。
当然,由于所有这些操作都要在一个时钟周期内并行完成,所以上述操作非常复杂。
在流水线的后端,我们必须能够在一个时钟周期内完成和提交多条指令。由于可以在同一时钟周期中提交多条指令必须已经解决任何依赖性问题,所以这些步骤要比发射问题容易一些。
当存在数据依赖分支时,推测方法会带来一些好处,反之则会限制性能。
3.9 用于指令交付和推测的高级技术
在多发射流水线中,仅仅很好地预测分支还不够,实际上还得能够交付高带宽的指令流。
3.9.1 提高取指带宽
多发射处理器要求每个时钟周期提取的平均指令数至少等于平均吞吐量。
分支目标缓冲区:
为了减少简单的无几流水线以及深流水线的分支代价,必须知道尚未译码的指令是不是分支,如果是分支,则需要知道下一个PC应当是什么。如果这条指令是一个分支,而且知道下一个PC应当是什么,那就可以将分支代价降为零。用于存储分支之后下一条指令的预测地址的分支预测缓存,称为分支目标缓冲区或分支目标缓存。
如果在分支目标缓冲区内找到一个匹配项,则立即在所预测的PC出开始取指。注意,与分支预测缓冲区不同,预测项必须与这条指令匹配,因为在知道这条指令是否为分支之前,预测的PC将发生出去。如果处理器没有查看这一项是否与这个PC匹配,就会为分支的指令发送错误的PC,从而导致性能恶化。
如果在分支预测缓冲区内找到了分支预测项,而且预测正确,那就没有分支延迟。否则,至少存在两个时钟周期的代价。我们在重写缓冲区条目时通常会暂停取指,所以要处理错误预测与缺失是一个不小的难题。
分支目标缓冲区的一种变体是存储一个或多个目标指令,作为预测目标地址的补充或替代,潜在优点有二:
(1)它允许分支目标缓冲区访问的时间长于两次取指之间的时间,从而可能允许采用更大的分支目标缓冲区,
(2)通过缓冲实现的目标指令,可以执行“分支折合branch folding”的优化。分支折合用于实现0时钟周期的无条件分支,有时也用于实现0时钟周期的条件分支。
3.9.2 专用分支预测器:预测过程返回、间接跳转和循环分支
尽管过程返回操作可以用分支目标预测缓冲区来预测,但如果从多个地方调用这个过程,而且来自一个地方的多个调用在时间上比较分散,那么这种预测方法的准确性就会降低。
集成取指单元:由于多发射的复杂性,不能再将取指过程视为简单的单一流水线,于是最近的设计开始使用集成了多种功能的集成取指单元,功能如下:
(1)集成分支预测:分支预测器变为取指单元的一部分,它持续预测分支,以驱动提取流水线。
(2)指令预取:为了在每个时钟周期内交付多条指令,取指单元可能需要预取指。这一单元自主管理指令的预取,把它与分支预测结合在一起。
(3)指令存储器访问与缓存:在每个时钟周期提取多条指令时会遇到不同的复杂性,包括提取多条指令可能需要访问多个缓存行,这是一个难题。取指单元封装了这一复杂性。尝试使用预取来隐藏跨缓存模块的成本。取指单元还可以提供缓存功能,大体充当一个随需应变单元,根据需要向发射级提供相应数量的指令。
3.9.3推测:实现问题与扩展
1. 推测支持:寄存器重命名与重排序缓冲区
ROB的一种替代方法是显式使用更大的物理寄存器集,并与寄存器重命名方法相结合。
在寄存器重命名方法中,使用物理寄存器的一个扩展集来保存体系结构可见寄存器和临时值。因此,扩展后的寄存器取代了ROB和保留站的大多数功能;只需要一个队列来确保顺序完成指令。在指令发射期间,一种重命名过程会将体系结构寄存器的名称映射到扩展寄存器集中的物理寄存器编号,为目的地分配一个新的未使用寄存器。WAR和WAW冒险通过重命名目标寄存器来避免。在指令提交之前。保存指令目的地的物理寄存器不会成为体系结构寄存器,所以也解决了推测恢复问题。
重命名是一种简单的数据结构,它提供当前与指定体系结构寄存器相对应的寄存器的物理寄存器编号。在Tomasulo算法中,这一功能由寄存器状态表完成。在提交指令时,重命名表被永久更新,以表明一个物理寄存器与实际体系结构寄存器相对应,从而有效地完成对处理器状态的更新。
与ROB方法相比,重命名方法的一个优点是略微简化了指令提交过程,因为它只需要2个简单的操作:
(1)记录体系结构寄存器编号与物理寄存器编号之间的映射不再是推测结果;
(2)释放所有用于保存体系结构寄存器“旧”值的物理寄存器。
在采用寄存器重命名时,撤销寄存器的分配要更复杂一些。这是因为在释放物理寄存器之前,必须知道它不再与体系结构寄存器相对应,而且对该物理寄存器的所有使用都已完成。
如果一个给定物理寄存器不是源寄存器,而且它也没有被指定为体系结构寄存器,那就可以收回该寄存器,重新进行分配。或者处理器也可以一直等待,直到对同一体系结构寄存器执行写操作的另一指令提交为止。
无论是重命名还是重排序缓冲区,动态调度超标量的关键复杂性瓶颈仍然在于所发射的指令包中存在依赖关系。
多发射使用的策略如下:
(1)发射逻辑预先为整个发射包预留足够的物理寄存器(比如,当每个指令最多有一个寄存器结果时,为4指令包预留4个寄存器);
(2)发射逻辑判断包中存在什么样的依赖关系。如果包中不存在依赖关系,则使用寄存器重命名结构来判断那个物理寄存器保存着(或将保存)指令所依赖的结果。如果包中不存在依赖关系,则结果源于先前的一个发射包,并且寄存器重命名表将拥有正确的寄存器编号。
(3)如果一个指令依赖于在该发射包中前列的某条指令,那么将使用在其中存放结果预留物理寄存器来为发射指令更新信息。
注意,与ROB一样,发射逻辑必须在一个周期内判断包中依赖关系,并更新重命名表,而且和以前一样,当每个时钟处理大量指令时,这种做法的复杂度就会成为发射宽度中的一个主要限制。
2. 每个周期发射更多指令带来的挑战
如果没有推测,人们就没有太大动力区尝试提高发射速率,使其达到每个时钟发射多条指令,因为分支的解析会将平均发射速率限制在一个更小的数值。一旦处理器中包含了准确的分支预测和推测,可能会认为提高发射速率是有吸引力的。
必须在一个周期内完成检测所有依赖关系,指派物理寄存器,使用物理寄存器编号重写这些指令。
3. 推测的代价
推测的一个重要优势是能够尽早发现那些本来会使流水线停顿的事件,比如缓存缺失。代价则是推测不是免费的,它需要时间和精力,错误推测的恢复还会进一步降低性能。此外,为了从推测中受益,需要支持更高的指令执行速率,为此,处理器必须拥有更多的资源,而这些资源又会占据硅面积和功耗。最后,如果推测导致异常事件的发生,比如缓存缺失或TLB缺失,而没有推测时不会发生这种事件,那推测造成的重大性能损失的可能性就会增大。
为了在最大程度上保持优势,减少不利因素,大多数具有推测的流水线仅允许以推测模式处理低成本的异常事件(比如,L1缓存缺失)。如果发生了高昂的异常事件(如L2缓存缺失或TLB缺失),那么处理器会一直等待,直到引发该事件的指令不再具有推测性时在处理该事件。
在20世纪90年代,推测的潜在缺点还不是很明显。随着处理器的发展,推测的实际成本变得越发明显,宽发射和推测的局限性也变得更为突出。
4. 多分支推测
之前是先解决一个分支,然后再推测另一个分支。有3种情形可以通过同时推测多个分支受益:
(1)分支频率高;
(2)分支高度汇集;
(3)功能单元中的延迟很长。
数据库和其他结构化程度较低的整数计算经常呈现这个特性。
5. 推测和能效的挑战
因为只要推测错误,就会以下面两种方式产生更高的能耗:
(1)对某些指令进行了推测,却不需要它们的结果;
(2)撤销推测,恢复处理器的状态,以便在适当的地址继续执行。
对于整型应用程序而言,推测过程的能效不会很高,而且登纳德缩放定律的终结也使用非完美推测的问题变得更加严重。设计人员可能会避免推测,尝试减少错误推测,或者考虑使用新方法,比如仅对那些已经确定具有高度可预测性的分支进行推测。
6. 地址别名预测
地址别名预测用来预测两个存储指令或者一个载入指令与一个存储指令是否引用同一存储器地址。因为我们不需要准确预测地址值,只需要直到这些值是否冲突,所以使用小预测器,预测就会非常准确。地址预测依赖于支持推测执行的处理器在错误推测后恢复的能力。
地址预测是一种简单的受限的值预测。值预测试图预测一条指令可能产生的值。如果值预测具有很高的准确性,那么它就可以消除数据流的限制,从而实现更高的ILP率。
3.10 交叉问题
3.10.1 硬件推测与软件推测
(1)为了进行广泛的推测,必须能够消除存储器访问的歧义。对于包含指针的整数程序,很难在编译时做到。在基于硬件的方案中,存储器地址的动态运行时消除帮助把载入指令移到存储指令之后。对推测性存储器访问的支持可以帮助克服编译器的保守性。
(2)当控制流不可预测,且基于硬件的分支预测优于在编译时完成的软件分支预测时,基于硬件的推测效果更加。这些特性适用于整数程序,其中动态预测器的错误预测率通常小于静态预测器的一半。由于推测错误的指令可能会拖慢计算速度,所以静态调度的处理器通常也包含动态分支预测器。
(3)即使对于被推测的指令,基于硬件的推测也能保持完全精确的异常模型。
(4)基于硬件的推测不需要补充或记录代码,而那些激进的软件推测机制则需要。
(5)基于编译器的方法能够深入了解代码序列,并从中获益,因而在代码调度方面要优于纯硬件驱动的方法。
(6)对于一种体系结构的不同实现,采用动态调度的基于硬件的推测不需要采用不同代码序列就能获得好的性能。
在硬件中支持推测的主要缺点是增加了复杂性,并且需要额外的硬件资源。必须根据基于软件的方法的编译器的复杂性,以及依赖于这种编译器的处理器中简化的程度和有用性,评估这种硬件成本。
意识到3.11节讨论的困难之后,设计人员利用ILP的热情已经减退,因此,大多数体系结构最终采用了基于硬件的方案,每个时钟周期发射3~4条指令。
3.10.2 推测执行与存储器系统
在支持推测执行或条件指令的处理器中,固有的一种可能性是生成一些无效地址,而在没有推测执行时是不会生成这些无效地址的。如果引发了保护异常,那这不仅是一种错误的行为,而且推测执行的收益还会被错误异常的开销抵消。因此,存储器系统必须识别推测执行的指令和条件执行的指令,并抑制相应的异常。
出于类似的原因,不能允许此类指令导致缓存因缺失而停顿,因为不必要的停顿可能会超过推测带来的收益。因此,这些处理器必须与非阻塞缓存相匹配。
3.11 多线程:利用线程级并行以提高单处理器吞吐量
线程就像一个进程,它有状态和当前的程序计数器,但是多个线程通常共享单个进程的地址空间,因而一个线程可以轻松地访问同一进程中其他线程的数据。在多线程技术中,多个线程共享一个处理器,而不需要进程切换。快速切换线程的能力使得多线程可以用来隐藏流水线和存储器延迟。
多线程与多重处理的结合。以及多线程是向硬件暴露更多并行机会的主要技术。多线程也可以提高流水线利用率以及在GPU中扮演的角色。
尽管ILP对编程人员透明的特点使之在提高性能方面具有很大的优势,但在某些应用程序中,ILP可能受到很大的限制或者难以利用。具体来说,当指令发射率出于合理范围,那些达到存储器或片外缓存的缓存缺失不太可能通过可用的ILP来弥补。当然,当处理器停顿下来等待缓存缺失,功能单元的利用率会急剧下降。
由于试图利用更多的ILP来掩盖长时间的存储器停顿的效果有限,所以人们会问:是否可以使用应用程序中其他形式的并行来掩盖存储器的延迟呢?多请求,并行3维建模。多活动应用程序等级别。
多线程技术支持多个线程以重叠方式共享单个处理器的功能单元。与之相对,利用TLP的更一般方法是多处理器。它同时并行运行多个独立线程。但是,多线程不会像多处理器那样复制整个处理器,而是在一组线程之间共享处理器核的大多数功能,仅复制私有状态,比如寄存器和PC。之后章节会看到在一个芯片上集成多个处理器核,又在每个核中提供多线程。
要复制处理器核中每个线程的状态,就要为每个线程创建一个单独的寄存器堆和一个单独的PC。存储器本身可以通过虚拟存储器机制共享,这些机制已经支持多道程序了。此外,硬件还必须支持对不同的线程快速修改。具体而言,线程切换的效率应当远远高于进程切换,后者通常需要数百到上千个处理器周期。
实现多线程的硬件方法主要有3种:细粒度多线程、粗粒度多线程和同时多线程。
(1)细粒度多线程:每个时钟周期都在线程之间切换,使多个线程的指令执行过程交织在一起。这种交织通常是以轮询方式完成的,当时发生停顿的的所有线程都会被跳过。优点在于可以隐藏短时间和长时间停顿所造成的吞吐量损失,因为当一个线程停顿时,可以执行来自其他线程的指令。缺点是减缓单个线程的执行速度,因为一个做好执行准备,没有停顿的线程会被其他线程的指令延迟。它用单个线程的性能损失(以延迟来衡量)来换取多线程吞吐量的增加。
(2)粗粒度多线程:是作为细粒度多线程的替代方法被发明的。仅在发生成本较高的停顿时才切换线程,比如L2或L3缓存缺失。仅当一个线程遇到高昂的停顿时才会发射其他线程的指令,所以粗粒度多线程不强求线程切换无成本,同时也大大降低了减缓任意线程执行速度的可能性。缺点在于克服吞吐量损失的能力有限,特别是由于较短停顿导致的损失。这一限制来自于粗粒度多线程的流水线启动成本。由于采取粗粒度多线程的处理器从单个线程发射指令,所以当流水线发生停顿时,在新线程开始执行之前会出现“气泡”。由于启动开销,粗粒度多线程对于减少成本非常高的停顿的损失非常有用,重新填充流水线的时间与停顿时间相比可以忽略不计。
(3)最常见的多线程实现方式称为同时多线程SMT。同时多线程是细粒度多线程的一种变体,它是在多发射、动态调度处理器上实现细粒度多线程时自然出现的。SMT利用线程级并行来隐藏处理器中的长延迟事件,从而提高功能单元的利用率。SMT关键在于通过寄存器重命名和动态调度可以执行来自独立线程的多条指令,而不用考虑这些指令之间的依赖关系。
在不支持多线程的超标量中,由于缺乏ILP(包括用于隐藏存储器延迟的ILP),所以发射槽的使用非常有限;
在粗粒度多线程超标量汇总,通过切换到另一个利用处理器资源的线程来部分隐藏长时间的停顿,减少了完全空闲的时钟周期数。但仅在存在停顿时才会切换线程。
在细粒度多线程超标量中,线程的交织可以消除全空槽。由于指令发射和执行联系在一起,所以线程的发射的指令仅限于准备就绪的指令。当发射宽度较窄时,这不是问题,这也就是细粒度多线程对于单发射处理器比较有用,而SMT则没有什么意义的原因。
如果在多发射的动态调度处理器的基础上实现细粒度线程,所得到的结果就是SMT。多线程和SMT在发射宽度较大时的动态调度处理器中潜在的性能优势。
SMT:动态调度处理器已经拥有支持该方案所需的许多硬件基础,包括一个大型虚拟寄存器集。通过为每个线程添加重命名表、保持独立的PC、支持提交来自多个线程的指令,也可以在乱序处理器上实现多线程。
3.13 谬论与易犯错误
谬论 如果保持技术稳定,很容易预测同一指令集体系结构的两个版本的性能与能效。
谬论:CPI较低的处理器总是更快一点。
谬论:时钟频率较快的处理器总是更快一点。
要点在于:性能是由CPI和时钟频率的乘积决定的。在通过实现CPU的深度流水化获得较高是时钟频率后,还必须保持较低的CPI,才能全面体现快速时钟频率的优势。
易犯错误: 有时,越大,越被动,就越好
在利用ILP时,主要限制因素是存储器系统。尽管推测乱序流水线可以很好地隐藏L1缺失的大部分时间,但它们几乎无法隐藏L2缺失。结果就是开始了从高ILP向多核方案转变。以及设计人员不再试图用ILP来隐藏更多的存储器延迟,而是利用晶体管来创建更大的缓存。
易犯错误: 有时,更聪明要好于更大和更被动
在过去十年里,最令人惊讶的结果之一出现在分支预测中。通过提高预测准确率,使对一个分支的预测不再被错误地用于另一个分支,这样获得的好处足以证明,将原本用来保存预测的位数分配给标签是恰当的。
易犯错误:只要拥有了正确的技术,就有大量的ILP可用
尝试大量利用ILP失败的原因在与采用传统结构的程序中很难找到大量的ILP,即使使用了推测也是如此。假设处理器具有以下特性:
(1)每个时钟周期可以执行64次发射和分发操作,不存在发射限制;当发射速率很高时,会出现严重的复杂性和功耗问题;
(2)拥有1000条竞争预测器和16个函数返回预测器;预测器不是瓶颈。错误预测在一个周期内得到出全力,但它们限制了推测能力。
(3)以动态方式完美消除存储器访问的歧义。
(4)寄存器重命名拥有64个整数寄存器和64个浮点寄存器。
结果却是使用大量的硬件只能得到些许的性能提升,并且错误的推测带来了严重的能耗问题。
3.14 路在何方
2000年左右,人们对ILP的关注达到顶峰。2005之后,Intel将重点放在多核。更高的性能将通过线程级并行而不是指令级并行来实现,而高效运行处理器的责任从硬件转移到软件和程序员身上。自20世纪中晚期使用流水线和ILP以来,这是处理器体系结构发生的最重大的变化。
在同一时期,设计人员开始利用DLP,SIMD扩展,GPU。
许多研究人员预测ILP的应用会大幅减少,并且未来会是双发射超标量和多核的天下。但是,略高的发射率以及使用推测动态调度来处理意外事件的优势,使得适当的ILP(每周期4发射)成为多核设计的主要模块。SMT的添加及有效性(性能或能耗)都进一步巩固了适度发射、乱序、推测执行方法的地位。
未来处理器几乎不会尝试大幅提高发射宽度,因为从硅利用率和能耗角度来看,它的效率太低了。多核和大缓存是趋势。