cpu性能提高术

overview

cpu 性能的两个指标：响应时间和吞吐率；

本篇文章将着重介绍以下几点：

流水线技术

利用好流水线技术里遇到的三大冒险

乱序执行

超标量技术提高吞吐率

流水线技术

一条 cpu 指令的执行过程可以简单划分为以下三步：

取指令

指令译码

指令执行

由于取指令时需要依靠时钟信号来让 pc 寄存器自增，所以一条指令的完成至少需要一个时钟周期；

按照最初的设计，我们希望性能最好，于是我们需要在一个时钟周期内完成且仅完成一条指令的处理，因为每一条指令的复杂度是不一样的，而时钟周期是固定的，所以时钟周期的长度必须达到执行时间最久的那一条指令；

这种设计的缺陷也很明显：非常浪费时间和资源；

不论我们运行的指令复杂还是简单，都必须要等满一整个时钟周期后才能运行下一条指令，而在这期间，cpu 很多资源，比如 ALU，很可能长时间处于闲置状态，造成资源的浪费；

至此，cpu 需要一种技术，能够使其尽可能地避免运行指令时时间与 cpu 硬件资源浪费的问题；

现代 cpu 往往采用流水线技术来解决这个问题；

简单来讲，流水线是一种实现多条指令重叠执行的过程，我们将一个任务拆分成几个步骤，一条流水线能够处理一条指令的各个步骤，而同一时刻，一个任务只会处于一个步骤，在第一个任务进入流水线，并从步骤 1 到达步骤 2 后，流水线处理步骤 1 的部分就空出来了，第二个任务就可以进入流水线进行步骤 1，而不必等到第一个任务完全结束再进入；

具体到指令的流水线化，需要定义流水线级别数，也就是上文所说的步骤数；

不同 cpu 有不同的级别数，有的性能敏感分地精细，有十几层，有的则只有几层；

我们以 ARMv8 的子指令集 —LEGv8 为例，划分为五个处理步骤：

从指令寄存器中取指令

读取寄存器并通过地址译码器译码

执行指令

读写内存

写回寄存器

每一个流水线级别的操作，都需要占用一个时钟周期，一条指令最终需要流水线级别数个时钟周期；

时钟周期长度也不再需要达到一条指令运行完的时间，只需要达到最复杂的流水线级别的操作的时长即可；

与此同时，单条指令的执行时间并没有因为流水线技术而得到优化，流水线是通过增加指令的吞吐率来提高性能的；

此外，流水线级别数也不能过多，因为流水线深度的增加是有成本的；

每一级流水线的输出，都需要保存到额外的流水线寄存器中，虽然流水线寄存器读写速度很快，但一旦层级数目多了起来，比如达到三四十，那也会降低响应的时间，而且还会增大功耗。

流水线三大冒险

流水线会出现这样一种情况：在下一个时钟周期中下一条指令无法执行，我们将这种情况称为冒险（hazard)；

所谓冒险，虽遇危机，但也必然有克服危机后的收获，在流水线技术中的冒险，假使我们好好解决遇到的问题，那能使 cpu 的吞吐率再上一个台阶；

流水线技术中会遇到三种冒险：结构冒险，数据冒险，控制冒险；

结构冒险

结构冒险的本质，是因硬件资源不足而造成的资源竞争问题；

在流水线中，处于不同阶段的两个指令假如都需要同样的电路资源，而这一种电路资源又恰好只有一份，这时就会发生结构冒险，使得指令无法顺利运行；

以上文划分的那五个步骤为例，第二步和第四步都需要用到地址译码器，由于只有一个地址译码器，取指令地址和取数据所在内存的地址会发生资源冲突；

解决办法有两个；

一个是将内存拆分为两部分，一部分放数据，一部分放指令，各自拥有各自的译码器，缺陷很明显：由于内存一分为二，没法动态分配，失去了灵活性；

还有一个，是将 cpu 内部的高速缓存分为指令缓存和数据缓存两部分，这样既可以解决结构冒险中的资源冲突问题，也不妨碍内存的动态分配；

前一种设计叫做哈佛架构，而后一种设计则被现代的冯・诺依曼体系结构所采用；

数据冒险

定义为：当一个步骤必须等待另外一个步骤完成才能进行（即存在数据依赖），此时将产生流水线的停顿，也就是数据冒险；

比如以下这段代码：

Copy
a :=1
b :=2
a = a+1
b = a+1

变量 b 的最终值依赖于变量 a 在执行完 add 1 之后的结果，这个顺序必须要保证；

最简单的方案是通过 ** 流水线冒泡，** 即：在译码完成之后，对该指令判断是否存在数据依赖，如果存在，则通过插入 NOP 操作（代表了什么都不干）的方式来达到流水线停顿，等到数据依赖问题解决了的那个时钟周期再开始正式执行（流水线是依靠时钟信号进行工作的，不可能真的停下来，所以需要依靠这个 NOP 操作）；此方案对 cpu 性能消耗比较大；

一个更为高级的策略是操作数前推，或者叫做操作数旁路；

我们以下面两条指令为例：

Copy
add $t0, $s2,$s1
add $s2, $s1,$t0

第二条指令的执行依赖于 t0 中的值，按照之前的流水线冒泡策略，我们必须等待第一条指令的执行结果写回 t0 后才能运行；但事实上，第二条指令完全无需等待 s2+s1 的结果写回 t0 后再执行，s1+s2 的结果可以直接传给第二条指令作为 t0 的输入，只要在电路中增加增加一些线路出来即可实现；这就是操作数前推的具体表现，其中增加的线路称为旁路；

通过操作数前推，可以有效减少 NOP 的数量，进一步提高流水线效率；

控制冒险

高级程序中的 for 循环和 if 语句，到了汇编代码，都会生成 cmp 比较指令和 jmp 等跳转指令，下一条指令究竟是 pc 寄存器中的值，还是 jmp 对应的地址，cpu 必须等待执行完 cmp 指令后才知道；当决策依赖于一条指令的结果，而其他指令正在执行，这就是控制冒险；

为了防止流水线停顿，我们需要在 cmp 的结果出来之前就对其进行一个预测，这就是分支预测技术；

分支预测技术也分很多种，最简单的叫做 ** 静态分支预测；**cpu 认定跳转不会发生，一直按照顺序执行下去，假如预测错误，就要丢弃后面执行的指令，这个丢弃的操作是有一定的性能开销的；

高级一点的叫做动态预测；常见的一种动态预测方案叫做一级分支预测，也叫 1 比特饱和计数，实质为：用一个比特去记录当前分支的比较情况，用这个值，取预测下一次分支的情况，比如，这一次跳转了，那么下一次就去跳转，否则下一次继续顺序执行；还有一种叫做 2 比特饱和计数，引入了状态机，相当于下一次分支的预测取决于前两次的结果；关于分支预测更详细的内容，可以参考wiki

乱序执行

上文数据冒险中的操作数前推可以减少很多不必要的 NOP 操作，但是有些情况下，NOP 是不可避免的，为了进一步提高吞吐率，cpu 完全可以在 NOP 这个阶段去运行后面没有依赖的，可以直接运行的指令，这种技术叫做乱序执行；

以下为过程图：

在保留站，一个指令等待其依赖的数据也传过来之后，就可以发给功能单元 FU 来执行，这个阶段就是乱序执行；

所谓的 FU，其实就是 ALU，但不同 ALU 可以有不同功能，所以乱序执行阶段需要尽可能最大化利用这一点；

执行完后，还要放入重排序缓冲区，排成原先指令的顺序；

其次，指令计算的结果先提交到存储缓冲区，最后再到 cpu 的高速缓存或是内存；

在访问内存和写回的阶段，必须是顺序的，这是为了防止多核造成的数据不一致问题：每个 cpu 核都有自己的缓存和寄存器，假如写内存的顺序不一致，就会出现预期之外的错误；

在这一过程中，从外部看依旧是顺序的，而内部各个 FU 都没有闲置，cpu 吞吐率进一步提提升；

超标量技术

即使上文各种 cpu 性能提高技术都用上，IPC（一个时钟周期能够执行的指令数）也不会超过 1，因为 cpu 在一个时钟周期只能取得一条指令；

即然指令的执行阶段可以通过乱序执行技术来并行，那取指令和指令译码也可以考虑采用并行，即：一次性从内存取出多条指令，发给多个指令译码器进行指令译码：

这种技术，叫做多发射与超标量：

现代 cpu 采取这种技术，使得 IPC 往往可以达到 2 以上。

Conclusion

本文的目的为介绍 cpu 性能提高的常见技术；

文章从流水线的介绍讲起，到流水线的三大冒险，涉及拆分高速缓存，流水线冒泡，操作数前推，分支预测等内容，再到乱序执行和超标量技术，用并行的方式更进一步利用资源，提高 cpu 的吞吐率。

掌握这些内容，即可对现代 cpu 性能提高有一个比较宏观的了解。