计算机组成原理学习笔记(4)——流水线设计

Posted by 皮皮潘 on 03-10,2022

单指令周期处理器

正如前面的博文所说,最简单的CPU执行方式就是每一个时钟周期执行一条指令,也就是所谓的单指令周期处理器,通过将数据通路连接起来,其大致示意图如下图所示:
image.png

image.png

其中红色的是控制信号,结合N-1选择器,控制信号可以决定每个逻辑电路单元的输入是什么,而黑色的则代表数据或者地址的传递。

可以看到在每一个时钟周期的上升沿都会触发指令寄存器的写入,进而触发逻辑电路的运行从而完成一条指令的执行。

整个过程很简单,但是它的缺点也同样非常致命:任何指令所需要的时间都是一样的(哪怕已经执行完了,也要进行等待,因为时钟周期上升沿还没到,寄存器还不能写入),从而使得时钟周期需要迁就于最复杂的指令周期,这导致了虽然CPI等于1了,但是时钟周期却大大增加了,完全无法利用目前CPU高主频的优势

流水线设计

为了解决单指令周期处理器中时钟周期受限于最复杂指令周期的问题,现代CPU提出了流水线设计

流水线设计的核心在于一条指令的实际运行其实是由多个阶段或者说多个电路模块构成的,我们在取指令的时候,需要一个译码器把数据从内存里面取出来,写入到寄存器中;在指令译码的时候,我们需要另外一个译码器,把指令解析成对应的控制信号、内存地址和数据,并对于寄存器模块而言将对应寄存器的值取出;到了执行的时候,我们需要的则是一个完成计算工作的 ALU 来进行对应的算法运算。这些都是一个一个独立的组合逻辑电路。

电路模块和电路模块或者说阶段和阶段之间是独立的,因此我们可以将时钟周期划分给阶段(阶段粒度比指令更小因此也更容易对齐同时所需要的时间也更小),这样的话在同一个时钟周期,不同的电路模块可以运行不同的指令的不同阶段,并通过流水线寄存器来实现阶段之间的同步(因为只有在时钟上升沿才会触发寄存器的更新,从而进一步触发之后的电路的更新,对于逻辑电路而言其计算虽然有门延迟但是也非常快的,因此寄存器的写入的等待会把它们给卡着从而完成同步),通过流水线的设计,虽然一条指令可能需要7,8个时钟周期才能完成,但是一方面时钟周期不再依赖于最复杂的指令周期了而是最复杂的阶段周期从而大大减少了时钟周期,另外一方面一次时钟周期会并行地同时执行不同指令的不同阶段也就相当于指令并行了,因此平摊下来CPI还是1,但是时钟周期大大减小了。

目前在教科书上常用的一个流水线设计是五级流水线:取指令-译码-执行-访存-写回,如下图所示:
image.png
这里需要注意的是,执行阶段的执行命名可能会有点误导,它其实并不是执行整条指令,而是完成指令中需要计算的算术部分的执行,比如在load指令中,它由于计算内存地址,而真正的load的执行则在访存和写回中。在这种 Pipeline 设计中,所有的计算指令只能针对寄存器和常数进行(写回这一阶段只能写回到寄存器中),对于内存的访问需要使用 loadstore 指令,也就是说类似如下的汇编指令: Add $1 %(0x110) 其实会被拆解为三条指令:

1. Load r1 %(0x110)`
2. Add r1 $1
3. Store %(0x110) r1

另外需要注意的是,流水线技术并不能缩短单条指令的响应时间这个性能指标,但是可以增加在运行很多条指令时候的吞吐率,对于单条指令而言,它的流水线是n级的,它就需要n个时钟周期才能完成,另外由于阶段间寄存器的写入,会导致响应时间更长,但是由于指令之间是几乎并行的(阶段并行),因此虽然第一条指令是在第n个时钟周期完成的,但是第二条指令在第n+1时钟周期完成了,因此在执行多条指令时,每条的平均CPI是1。

超长流水线

既然流水线可以增加我们的吞吐率,为什么现代最长的流水线级别只有14级,CPU厂商为什么不把流水线级数做得更深呢?

性能成本

一个最基本的原因,就是增加流水线深度,其实是有性能成本的。正如前文所说,我们用来同步时钟周期的,不再是指令级别的,而是流水线阶段级别的。每一级流水线对应的输出,都要放到流水线寄存器(Pipeline Register)里面,然后在下一个时钟周期,交给下一个流水线级去处理。所以,每增加一级的流水线,就要多一级写入到流水线寄存器的操作。虽然流水线寄存器非常快,比如只有 20 皮秒(ps,10−12 秒)。

1730_1.png

但是,如果我们不断加深流水线,这些操作占整个指令的执行时间的比例就会不断增加。最后,我们的性能瓶颈就会出现在这些 overhead 上。如果我们指令的执行有 3 纳秒,也就是 3000 皮秒。我们需要 20 级的流水线,那流水线寄存器的写入就需要花费 400 皮秒,占了超过 10%。如果我们需要 50 级流水线,就要多花费 1 纳秒在流水线寄存器上,占到 25%。这也就意味着,单纯地增加流水线级数,不仅不能提升性能,反而会有更多的 overhead 的开销。

指令重排序

正如前文所说,流水线将一个指令划分为不同阶段,并且在同一个时钟周期并行地执行不同指令的不同阶段,理想情况下,当上一个指令开始阶段二的时候下一个指令就能开始阶段一了,但是我们来看一下如下代码:

int a = 10 + 5; // 指令1
int b = a * 2; // 指令2
float c = b * 1.0f; // 指令3

我们会发现,指令 2,不能在指令 1 的第一个 Stage 执行完成之后进行。因为指令 2,依赖指令 1 的计算结果。同样的,指令 3 也要依赖指令 2 的计算结果。这样,即使我们采用了流水线技术,这三条指令执行实际上也需要串行而无法并行,为了避免这种串行的情况发生,在一定情况下,我可以把后面没有依赖关系的指令放到前面来执行。这个就是我们所说的指令重排序的技术,但也正是指令重排序成为了流水线级数不能过多的另外一个原因,因为级数越多就要要求越多的指令之间没有依赖关系,而我们平时写的程序通常前后的代码都是有一定的依赖关系,因此这也导致了超长流水线的指令重排序会非常难,进一步降低了性能

参考

  1. 《深入浅出计算机组成原理》
  2. 《编码:隐匿在计算机软硬件背后的语言》
  3. Coursera-北京大学-《计算机组成》