20 | 面向流水线的指令设计(上):一心多用的现代CPU

前面我们用了三讲,用一个个的电路组合,制作出了一个完整功能的CPU。这里面一下子给你引入了三个“周期”的概念,分别是指令周期、机器周期(或者CPU周期)以及时钟周期。

你可能会有点摸不着头脑了,为什么小小一个CPU,有那么多的周期(Cycle)呢?我们在专栏一开始,不是把CPU的性能定义得非常清楚了吗?我们说程序的性能,是由三个因素相乘来衡量的,我们还专门说过“指令数×CPI×时钟周期”这个公式。这里面和周期相关的只有一个时钟周期,也就是我们CPU的主频倒数。当时讲的时候我们说,一个CPU的时钟周期可以认为是可以完成一条最简单的计算机指令的时间。

那么,为什么我们在构造CPU的时候,一下子出来了那么多个周期呢?这一讲,我就来为你说道说道,带你更深入地看看现代CPU是怎么一回事儿。

愿得一心人,白首不相离:单指令周期处理器

学过前面三讲,你现在应该知道,一条CPU指令的执行,是由“取得指令(Fetch)-指令译码(Decode)-执行指令(Execute) ”这样三个步骤组成的。这个执行过程,至少需要花费一个时钟周期。因为在取指令的时候,我们需要通过时钟周期的信号,来决定计数器的自增。

那么,很自然地,我们希望能确保让这样一整条指令的执行,在一个时钟周期内完成。这样,我们一个时钟周期可以执行一条指令,CPI也就是1,看起来就比执行一条指令需要多个时钟周期性能要好。采用这种设计思路的处理器,就叫作单指令周期处理器(Single Cycle Processor),也就是在一个时钟周期内,处理器正好能处理一条指令。

不过,我们的时钟周期是固定的,但是指令的电路复杂程度是不同的,所以实际一条指令执行的时间是不同的。在第13讲第14讲讲加法器和乘法器电路的时候,我给你看过,随着门电路层数的增加,由于门延迟的存在,位数多、计算复杂的指令需要的执行时间会更长。

不同指令的执行时间不同,但是我们需要让所有指令都在一个时钟周期内完成,那就只好把时钟周期和执行时间最长的那个指令设成一样。这就好比学校体育课1000米考试,我们要给这场考试预留的时间,肯定得和跑得最慢的那个同学一样。因为就算其他同学先跑完,也要等最慢的同学跑完间,我们才能进行下一项活动。

快速执行完成的指令,需要等待满一个时钟周期,才能执行下一条指令

所以,在单指令周期处理器里面,无论是执行一条用不到ALU的无条件跳转指令,还是一条计算起来电路特别复杂的浮点数乘法运算,我们都等要等满一个时钟周期。在这个情况下,虽然CPI能够保持在1,但是我们的时钟频率却没法太高。因为太高的话,有些复杂指令没有办法在一个时钟周期内运行完成。那么在下一个时钟周期到来,开始执行下一条指令的时候,前一条指令的执行结果可能还没有写入到寄存器里面。那下一条指令读取的数据就是不准确的,就会出现错误。

前一条指令的写入,在后一条指令的读取之前

到这里你会发现,这和我们之前第3讲第4讲讲时钟频率时候的说法不太一样。当时我们说,一个CPU时钟周期,可以认为是完成一条简单指令的时间。为什么到了这里,单指令周期处理器,反而变成了执行一条最复杂的指令的时间呢?

这是因为,无论是PC上使用的Intel CPU,还是手机上使用的ARM CPU,都不是单指令周期处理器,而是采用了一种叫作指令流水线(Instruction Pipeline)的技术。

无可奈何花落去,似曾相识燕归来:现代处理器的流水线设计

其实,CPU执行一条指令的过程和我们开发软件功能的过程很像。

如果我们想开发一个手机App上的功能,并不是找来一个工程师,告诉他“你把这个功能开发出来”,然后他就吭哧吭哧把功能开发出来。真实的情况是,无论只有一个工程师,还是有一个开发团队,我们都需要先对开发功能的过程进行切分,把这个过程变成“撰写需求文档、开发后台API、开发客户端App、测试、发布上线”这样多个独立的过程。每一个后面的步骤,都要依赖前面的步骤。

我们的指令执行过程也是一样的,它会拆分成“取指令、译码、执行”这样三大步骤。更细分一点的话,执行的过程,其实还包含从寄存器或者内存中读取数据,通过ALU进行运算,把结果写回到寄存器或者内存中。

如果我们有一个开发团队,我们不会让后端工程师开发完API之后,就歇着等待前台App的开发、测试乃至发布,而是会在客户端App开发的同时,着手下一个需求的后端API开发。那么,同样的思路我们可以一样应用在CPU执行指令的过程中。

通过过去三讲,你应该已经知道了,CPU的指令执行过程,其实也是由各个电路模块组成的。我们在取指令的时候,需要一个译码器把数据从内存里面取出来,写入到寄存器中;在指令译码的时候,我们需要另外一个译码器,把指令解析成对应的控制信号、内存地址和数据;到了指令执行的时候,我们需要的则是一个完成计算工作的ALU。这些都是一个一个独立的组合逻辑电路,我们可以把它们看作一个团队里面的产品经理、后端工程师和客户端工程师,共同协作来完成任务。

流水线执行示意图

这样一来,我们就不用把时钟周期设置成整条指令执行的时间,而是拆分成完成这样的一个一个小步骤需要的时间。同时,每一个阶段的电路在完成对应的任务之后,也不需要等待整个指令执行完成,而是可以直接执行下一条指令的对应阶段。

这就好像我们的后端程序员不需要等待功能上线,就会从产品经理手中拿到下一个需求,开始开发API。这样的协作模式,就是我们所说的指令流水线。这里面每一个独立的步骤,我们就称之为流水线阶段或者流水线级(Pipeline Stage)。

如果我们把一个指令拆分成“取指令-指令译码-执行指令”这样三个部分,那这就是一个三级的流水线。如果我们进一步把“执行指令”拆分成“ALU计算(指令执行)-内存访问-数据写回”,那么它就会变成一个五级的流水线。

五级的流水线,就表示我们在同一个时钟周期里面,同时运行五条指令的不同阶段。这个时候,虽然执行一条指令的时钟周期变成了5,但是我们可以把CPU的主频提得更高了。我们不需要确保最复杂的那条指令在时钟周期里面执行完成,而只要保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了。

如果某一个操作步骤的时间太长,我们就可以考虑把这个步骤,拆分成更多的步骤,让所有步骤需要执行的时间尽量都差不多长。这样,也就可以解决我们在单指令周期处理器中遇到的,性能瓶颈来自于最复杂的指令的问题。像我们现代的ARM或者Intel的CPU,流水线级数都已经到了14级。

虽然我们不能通过流水线,来减少单条指令执行的“延时”这个性能指标,但是,通过同时在执行多条指令的不同阶段,我们提升了CPU的“吞吐率”。在外部看来,我们的CPU好像是“一心多用”,在同一时间,同时执行5条不同指令的不同阶段。在CPU内部,其实它就像生产线一样,不同分工的组件不断处理上游传递下来的内容,而不需要等待单件商品生产完成之后,再启动下一件商品的生产过程。

超长流水线的性能瓶颈

既然流水线可以增加我们的吞吐率,你可能要问了,为什么我们不把流水线级数做得更深呢?为什么不做成20级,乃至40级呢?这个其实有很多原因,我在之后几讲里面会详细讲解。这里,我先讲一个最基本的原因,就是增加流水线深度,其实是有性能成本的。

我们用来同步时钟周期的,不再是指令级别的,而是流水线阶段级别的。每一级流水线对应的输出,都要放到流水线寄存器(Pipeline Register)里面,然后在下一个时钟周期,交给下一个流水线级去处理。所以,每增加一级的流水线,就要多一级写入到流水线寄存器的操作。虽然流水线寄存器非常快,比如只有20皮秒(ps,$10^{-12}$秒)。

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

总结延伸

讲到这里,相信你已经能够理解,为什么我们的CPU需要流水线设计了,也能把每一个流水线阶段在干什么,和上一讲的整个CPU的数据通路的连接过程对上了。

可以看到,为了能够不浪费CPU的性能,我们通过把指令的执行过程,切分成一个一个流水线级,来提升CPU的吞吐率。而我们本身的CPU的设计,又是由一个个独立的组合逻辑电路串接起来形成的,天然能够适合这样采用流水线“专业分工”的工作方式。

因为每一级的overhead,一味地增加流水线深度,并不能无限地提高性能。同样地,因为指令的执行不再是顺序地一条条执行,而是在上一条执行到一半的时候,下一条就已经启动了,所以也给我们的程序带来了很多挑战。这些挑战和对应的解决方案,就要请你坚持关注后面的几讲,我们一起来揭开答案了。

推荐阅读

想要了解CPU的流水线设计,可以参看《深入理解计算机系统》的4.4章节,以及《计算机组成与设计 硬件/软件接口》的4.5章节。

课后思考

我们在前面讲过,一个CPU的时钟周期,可以认为是完成一条简单指令的时间。在这一讲之后,你觉得这句话正确吗?为什么?在了解了CPU的流水线设计之后,你是怎么理解这句话的呢?

欢迎留言和我分享你的疑惑和见解。你也可以把今天的内容,分享给你的朋友,和他一起学习和进步。

精选留言

  • 易儿易

    2019-06-10 20:53:23

    一个 CPU 的时钟周期,可以认为是完成一条简单指令的时间。
    两个错误:
    1简单:不是简单是复杂(耗时最长)
    2指令:不是指令时间,而是指令拆分后线级执行时间
    简单线级需要等待以补齐时间然后与上下流水线一起牵手步入下一线级
    线级分的多的话,这个时钟周期应该就可以固定是IF取指令的时间了
    如果把IF取指令看做是最简单的指令的话,这句话就没毛病了
    但一般说指令应该是指令集的一个命令吧?
    但是指令集是提供给开发者视角的,在CPU内部IF取指令才是一个原子化操作,从CPU视角来看又觉得这句话是靠谱的……
    作者回复

    易儿易同学,

    你说得很好啊!👍

    2019-06-11 21:33:23

  • cc

    2019-06-10 08:37:48

    随着流水线设计的引入,一个指令被拆分为14个子流程。一个CPU的时钟时间,应该是14个子流程中最长的那条的耗时时间
    作者回复

    回答正确!👍

    2019-06-11 21:59:45

  • 随心而至

    2019-10-15 10:33:39

    1.高度的专业化分工,可以极大的提高生产效率。
    《国富论》中亚当斯密提到一个扣针工场的例子:一个工人无论如何努力,一天也生产不了20枚扣针,但有了分工之后,经过前后十几道工序,每人每天平均可以生产48000枚扣针。
    这可以从经济学上,解释为什么会有流水线
    2.边际效益递减
    新增一个单位的商品或服务,它对商品或服务的收益增加的效用,经济学通常认为,随着商品或服务的量增加,边际效用将会逐步减少。
    这可以从经济学上,解释为什么流水线级数不可以无限增加,而有个界限。
  • haoly1989

    2019-08-21 07:13:53

    如果从单条指令的执行来看,这句话显然是错的,因为即使最简单的无条件跳转指令都至少需要3个时钟周期;
    在了解CPU的流水线设计之后,这句话从吞吐率的角度来讲,是正确的:以5级流水线为例,当过了5个时钟周期后,可以保证每个时钟周期都有一条指令执行完成,即实现了一个CPU的时钟周期;由于取指阶段肯定是所有5个阶段里耗时最长的,即一个CPU的时钟周期可以设置为取指阶段的花费时间;

    注意,这里的5个时钟周期后,才有这种现象,意味着即使引入了流水线操作,这5个时钟周期的开销也是不可避免的,突然想到了之前讲到的Amdahl定律
    作者回复

    👍能够前后联系起来,很好啊

    2019-09-28 16:52:17

  •        鸟人

    2019-06-11 09:22:39

    这个不知道跟多线程 多进程是否有关系?
    作者回复

    这个和多线程多进程没有太大关系,那个更多是操作系统调度的问题,这个是CPU在硬件层面的实现。

    2019-06-11 21:13:55

  • 瀚海星尘

    2019-07-24 11:05:23

    各种工程都充满了权衡~
  • 喜欢吃鱼

    2019-06-10 12:06:05

    每个流水线级的执行时间应该不一定相同吧,所以一个CPU执行周期是不是执行时间最久的流水线级所需要的时间?
    作者回复

    喜欢吃鱼同学,你好

    准确地说,每一个流水线级的时间都是一个时钟周期,但是其中实际操作的时间,可能短于一个时钟周期。比如我们译码器其实就是一个组合逻辑电路,门延迟很低,就不需要一个完整的时钟周期就能完成自己的任务。那么在这个之后,它其实是在“等待”。

    2019-06-11 22:05:36

  • Only now

    2019-06-10 10:11:37

    一个 CPU 的时钟周期,可以认为是完成一条简单指令的时间。 这个应该是依据于吞吐量来理解的吧。
    实际指令不能在一个时钟周期完成, 但是流水线的引入使吞吐量更高。
    作者回复

    没错,这是一个很好的角度,从吞吐率的角度来讲,如果没有superscalar这样的指令集并行,那么流水线没有遇到任何冒险问题的情况下。按吞吐率算下来相当于一个时钟周期完成了一条指令。

    2019-06-11 22:08:52

  • 斐波那契

    2019-06-11 13:47:17

    这句话不正确
    哪怕再简单的指令都要经过划分好的阶段。首先我们要明确那几个周期的概念 所谓时钟周期(或叫做震荡周期)是指cpu一次晶体震动的时间 是计算机最小的时间单位 就拿我们补码来说 取反加1 这实际上就至少需要2个脉冲也就是两个时钟周期了 对于一条指令来说我们把执行过程划分成好几个阶段(具体几个由厂商工艺设计决定) 每个阶段的时间就是一个机器周期 所以一般情况下执行一条指令的时间是由多个机器周期决定的而一个机器周期又由多个时钟周期决定的 这样一条流水线就是由最多时钟周期的机器周期时间决定的 显然一个时钟周期执行不完一条指令
    作者回复

    Single-Cycle Processor就可以在一个时钟周期完成一条指令呀。虽然Single-Cycle Processor已经并不在现代CPU设计中使用了,但是并不代表这个是不可行的啊。

    2019-06-11 21:11:40

  • softpower2018

    2019-06-11 09:25:27

    引入了流水线技术,一个时钟周期执行一条简单指令应该完全没问题吧!期待多指令并行执行带来的问题及其解决!
    作者回复

    softpower2018同学,你好,

    其实现代流水线下的CPU,一个时钟周期已经不能执行完一条指令了。因为一般来说,取指令就要一个时钟钟周期……

    2019-06-11 21:13:22

  • Tiger

    2021-06-26 14:17:30

    老师,请问为什么访存会在执行阶段之后,不应该是先访存取出数据,再进行ALU运算,最后再将数据写回吗?
  • -W.LI-

    2019-06-22 23:18:25

    老师好!我又两个问题。
    指令分级以后同样会存在一个问题啊,每一级执行时间不一样。这样一个指令周期需要时最长的那个级的时间么?还有就是每条指令的级数一样么?如果一个时钟周期就是一级的时间,那么执行时间长的指令的级数就多了吧。再就是指令分级这个需要考虑原子性这种么?还是随便分分?
  • Ronnyz

    2019-12-13 10:35:18

    一个CPU时钟周期在指令流水中取决于指令拆分后最长的子阶段时间长度
  • 曾轼麟

    2019-06-24 09:17:20

    刚刚老师提到为什么流水线级数不能太高,其实在if else有一个分支预测的技术,如果级数太高预测错误,导致清空后续的流水线重新拉取指令的代价也是很高的
  • 南山

    2019-06-10 12:43:17

    无条件跳转这样的简单指令三个流水级应该可以在一个时钟周期完成,换句话说就是一个指令在一个时钟周期完成。但是稍微复杂的指令往往是超过一个时钟周期的。
    这里有一点疑惑:在流水线设计的CPU中,一个时钟周期只会执行一个流水级吗?对于很简单的流水级,可能执行时间只占时钟周期的一半,那另一半就是什么也不干吗?
    作者回复

    一个流水线级就需要一个时钟周期,所以即使是无条件跳转这样的简单指令,也需要三个时钟周期。

    是的,可能特定的流水线级的操作需要的时间很少,那么其他时间本质上是在“等待”。

    2019-06-11 22:03:58

  • Mamba

    2024-08-22 16:14:56

    一个 CPU 的时钟周期,最合理应当是一个最复杂的流水线级的操作所需耗时
  • 漂泊者

    2024-01-26 11:21:09

    如果我们有一个开发团队,我们不会让后端工程师开发完 API 之后,就歇着等待前台 App 的开发、测试乃至发布,而是会在客户端 App 开发的同时,着手下一个需求的后端 API 开发。那么,同样的思路我们可以一样应用在 CPU 执行指令的过程中。
    等着不行么,休息休息
  • Geek_88604f

    2023-11-02 07:47:15

    将一个复杂的流程拆分成不同的阶段,不同的阶段就可以并行执行形成流水线,提升吞吐量
  • 大凡人

    2023-09-24 17:04:27

    一个指令没处理完,就会交给下一个流水线去处理,直到完成,流水线的深度需要确保最复杂的指令可以完成
  • 小耿

    2022-07-19 11:45:34

    老师有一个问题:有没有可能下一条指令需要上一条指令的执行结果?如果像现在这样多条流水线并行执行,不也会出现前一条指令结果未写入,下一条指令已经开始读取的错误么?