
2.1 指令级并行
表面上看,处理器是一条又一条串行的执行指令,实际上可以同时对多条指令求值,这称为指令级并行。指令级并行要求同时执行的指令之间没有数据或控制依赖。指令级并行相关的技术主要有:指令流水线、乱序执行、多发射、VLIW和分支预测。
通过指令级并行,处理器可以调整程序中指令在该处理器上的执行顺序,能够处理某些在编译阶段无法知道的相关关系(如涉及内存引用时);在指令集兼容的条件下,能够允许一个流水线机器上编译的指令,在另一个流水线机器上也能有效运行。
指令级并行能够利用处理器上的不同组件同时工作,如果程序具有类型丰富的运算,指令级并行能使处理器性能迅速提高。
2.1.1 指令流水线
处理器有许多不同的功能单元,如果能够利用它们可以同时执行的特点,就可以提高执行速度。现代处理器将指令操作划分为许多不同的阶段,每个阶段由某个单元执行,这样存在多个操作在处理器的多个单元上像多个水流一样向前流动,这称为“流水线执行”,而每个水流就称为一个流水线。流水线(pipeline)是一串操作的集合,其中前一个操作的输出是下一个操作的输入。流水线执行允许在同一时钟周期内重叠执行多个指令。图2-1是一个取指令、指令解码、数据加载、操作和写回的经典5阶段流水线示意图。

图2-1 流水线示意图
为了充分利用流水线的好处,一些处理器将一些复杂的指令划分为许多更小的指令以增加流水线的长度。流水线系统中存在许多正在执行且还没有执行完的指令,现代处理器能够允许上百条流水线指令同时执行,而每条指令的延迟可能长达几个甚至几十个时钟,而最终的结果是某些指令的吞吐量达到每时钟周期几个。(如果能够达到每时钟周期超过一条指令的吞吐量,称之为超标量。)通常一条指令的计算和另一条指令的访存同时进行,这样能够更好地利用流水线的好处。
为了更好地利用指令流水线,现代处理器升级通常会增加指令流水线的长度,但是代码中指令级并行是有限的,一旦达到此限制,再增加流水线长度就不会有好处。如ARM A9的指令流水线长度为8,而A15为13。
带有长流水线的处理器想要达到最佳性能,需要程序给出高度可预测的控制流。代码主要在紧凑循环中执行的程序,可以提供恰当的控制流,比如大型矩阵或者在向量中做算术计算的程序。此时在大多数情况下处理器可以正确预测代码循环结束后的分支走向。在这种程序中,流水线可以一直保持在满状态,处理器高速运行。
2.1.2 乱序执行
对于按序处理器来说,一旦一条指令因为需要等待前面指令的结果,那么该指令之后的所有指令都需要等待。为了充分利用指令流水线或多个执行单元的好处,处理器引入了乱序执行的概念,乱序执行是指后一条指令比前一条指令先开始执行,但要求这两条指令没有数据及控制依赖。由于在很多情况下,处理器比较难以判断指令的数据相关性,编译器也引入了类似的功能,主要有指令重排和变量重命名。通常还需要软件开发人员以处理器和编译器友好的方式编写代码,以充分发掘应用具有的并行性,利用处理器的乱序执行功能。
乱序执行需要在执行指令前知道指令之间的依赖关系,如果两条指令之间有依赖,那么这两条指令就不能乱序执行。现代处理器对乱序执行有不同程度的支持,比如大多数的Intel X86桌面处理器和服务器处理器上都具有重排缓冲区(ReOrder Buffer,ROB),并且具有远多于逻辑寄存器数量的物理寄存器以支持寄存器重命名。
乱序执行会重排指令的执行顺序,这要求处理器的发射能力大于其执行能力。如果处理器的发射能力和指令的执行能力一致,那么ROB中就不会有指令等待重新排列执行顺序。由于处理器执行不同指令的速度并不相同,因此其发射能力并不一定比执行最快的指令的吞吐量大,比如主流X86 CPU一个周期能够处理4条整数加法指令,但是其指令发射能力也是一个周期4条。
2.1.3 指令多发射
从乱序执行的角度来说,处理器的发射能力最好要大于指令执行能力。而从处理器具有多个执行单元、每个执行单元能够在一个周期内同时执行多条指令的角度来看,如果指令发射单元每个周期只能发射一条指令,那么必定有些单元空闲。从这个角度来说,只要是具有多个执行单元的处理器,无论是否支持乱序执行,都要求其指令发射能力大于执行能力。
指令发射单元一个周期内会发射多条指令,通常指令发射单元的发射能力会超过单一硬件单元的处理能力,如在NVIDIA Kepler GPU上,SMX一个周期可以发射8条指令,但是SMX本身却最多能消耗6条乘加指令;如在主流的Intel X86 Haswell CPU上,一个周期可发射4条指令,但是只能消耗2条乘加指令。
许多处理器支持一个周期发射2条或多条指令,但是多条指令要满足一条条的条件,比如有些处理器要求没有依赖关系、有些处理器只允许访存指令和计算指令同时发射,而Intel Xeon PHI处理器两个周期可以为一个线程发射两条指令,但是这两条指令要没有背靠背的依赖。
指令多发射增加了硬件的复杂度,提高了处理器的指令级并行能力。
2.1.4 分支预测
当处理器遇上分支指令(判断指令,如if)的时候,有两种选择:
·直接执行下一条指令,如果分支是循环的判断条件,则这很有可能造成流水线中断;
·选择某条分支执行,一旦选择错误,处理器就需要丢弃已经执行的结果,且从正确的分支开始执行。
目前几乎所有的处理器都采用了后者,而选择哪个分支的过程称为“分支预测”,很多生产商宣称分支预测正确率达到90%以上(关于这一点,聪明的软件开发人员各有各的观点)。
如果程序中带有许多循环,且循环计数都比较小,或者面向对象的程序中带有许多虚函数的对象(每个对象都可以引用不同的虚函数实现),此时处理器很难或者完全不可能预测某个分支的走向。由于此时程序的控制流不可预测,因此处理器常常猜错。处理器需要频繁等待流水线被清空或等待被新指令填充,这将大幅降低处理器的性能。
关于分支预测,不同的处理器展现出完全不同的态度,比如X86就极力地优化其分支预测器的性能,而ARM和主流的GPU则要保守得多。
为了帮助处理器更好地进行分支预测,软件开发人员需要依据某些原则编写分支代码,可参考本书的第4章。
2.1.5 VLIW
乱序执行和分支预测增加了硬件的复杂性。在并行执行任何操作之前,处理器必须确认这些指令间没有相互依赖。乱序执行处理器增加了硬件资源用于调度指令和决定依赖。
而VLIW(Very Long Instruction Word)通过另外一种方法来实现指令级并行。VLIW的并行指令执行基于编译时已经确定好的调度。由于决定同时执行的工作是由编译器来完成的,处理器不再需要调度硬件。结果VLIW处理器相比其他多数的超标量处理器提供了更加强大的处理能力且更少的硬件复杂性。(硬件的复杂性降低了,但编译器的复杂性提高了。)
VLIW对于一些向量操作非常有效,并且能够组合某些不相关的指令以同时执行,但是VLIW对编译器提出了过高的要求,故实际性能通常并不是很理想。早期的AMD GPU大量使用了VLIW,现在已经全面转向使用SIMT。目前主要是移动端GPU和一些过时的AMD GPU/APU在使用VLIW。