跳转到内容
← 返回核心概念
计算机科学基础计算机科学 · 体系结构17 分钟阅读

CPU 流水线

CPU Pipelining

想象一个汽车组装工厂,有一百道工序。最简单的方式是:一辆车从头到尾完成所有工序后,再开始下一辆。如果每道工序需要 1 分钟,完成一辆车需要 100 分钟,每小时生产 0.6 辆。 更聪明的方式是流水线(Assembly Line):一辆车完成第一道工序后,立刻移向第二道,而另一辆新车进入第一道工序。在任何一个时刻,每道…

CPU流水线乱序执行分支预测微架构

想象一个汽车组装工厂,有一百道工序。最简单的方式是:一辆车从头到尾完成所有工序后,再开始下一辆。如果每道工序需要 1 分钟,完成一辆车需要 100 分钟,每小时生产 0.6 辆。

更聪明的方式是流水线(Assembly Line):一辆车完成第一道工序后,立刻移向第二道,而另一辆新车进入第一道工序。在任何一个时刻,每道工序都在同时处理一辆不同的车。满载后,每分钟完成一辆车,每小时生产 60 辆——提升了 100 倍。

1960 年代,处理器设计师把这个工业流水线的思想引入了 CPU 设计。这是现代高性能处理器最核心的技术之一。

最早把指令流水线付诸实践的是两台晶体管超级计算机。IBM 7030 Stretch 于 1961 年交付,它用一个"提前取指单元"(look-ahead unit)让取指、译码、变址与执行互相重叠,并预取最多 11 条指令填入缓冲区,是当时世界上最快的计算机。伊利诺伊大学的 ILLIAC II 在 1962 年投入运行,把流水线阶段命名为"超前控制、延迟控制与交互"。这些机器证明:只要把一条指令的执行切成可重叠的小段,硬件利用率就能成倍提升。

破除误解:流水线不是多核并行

人们常把流水线与多核处理器(同时运行多个程序)混为一谈。两者是不同的并行化维度:

  • 多核(多处理器):多条独立的流水线,真正的指令级并行,执行不同的线程
  • 流水线:单条流水线内部的并行,不同指令的不同阶段同时进行

一个 4 核 CPU,每个核内部都有一条完整的流水线。流水线是单个核的性能技术,多核是系统级并行技术。工厂比喻也有坑:汽车不会因为前面一辆漆错颜色就把后面的车退回。指令会。后一条要用前一条刚算出来的数,流水线就得停下或转发。分支一旦猜错,后面填进去的指令整段作废。吞吐上去了,单条指令的延迟并没有变成五分之一。宣传材料爱说"并行了 5 倍",硬件里付的是冒险(hazard)的税。

流水线的基本阶段

经典的 5 阶段 RISC 流水线(MIPS 架构为原型):

IF     →  ID    →  EX    →  MEM   →  WB
取指      译码     执行     内存访问   写回
(Instruction  (Instruction  (Execute)  (Memory   (Write-
 Fetch)        Decode)                  Access)   Back)
```

流水线时空图(展示多条指令的重叠执行):

指令 1:IF  ID  EX  MEM  WB
指令 2:    IF  ID  EX   MEM  WB
指令 3:        IF  ID   EX   MEM  WB
指令 4:            IF   ID   EX   MEM  WB
```

在第 5 个时钟周期,4 条指令同时处于流水线的不同阶段。理想情况下,5 阶段流水线使吞吐量提升 5 倍。理想只活在教科书的时空图里。相关指令一来,气泡插进去,有效加速比立刻掉到 5 以下。深度再加长,单条指令穿过流水线的时间反而更长,只是每隔一拍仍能交出一条结果。吞吐和延迟是两本账。Meltdown 与 Spectre 后来证明:为了填满流水线而做的预测和乱序,会在缓存时序上泄漏不该泄漏的东西。性能抽象会变成安全抽象的裂缝。

流水线的阻碍:冒险(Hazard)

现实中,流水线并非总能满载运行,存在三类冒险(Hazard)使流水线必须"停顿"(Stall):

数据冒险(Data Hazard)

一条指令依赖于前一条指令的结果,而结果还没准备好:

ADD R1, R2, R3    # R1 = R2 + R3(在 WB 阶段写回 R1)
SUB R4, R1, R5    # R4 = R1 - R5(需要 R1 的新值,但 ADD 还没写回)
```

解决方案: - 流水线停顿(Stall):等待前一条指令完成,插入"气泡"(NOP),简单但损失性能 - 数据前推(Data Forwarding / Bypassing):在 ADD 的 EX 阶段结果出来后,不等写回,直接传给 SUB 的 EX 阶段使用,消除大部分数据冒险

前推把结果从运算器侧面抄走,像工厂里的快速滑槽,不必等入库再领料。仍有抄不走的情况:load 的数据要等访存结束。于是编译器会把无关指令塞进空档,硬件也会停一拍。流水线的速度,最后卡在存储器和分支这两类不肯按时到齐的东西上。缓存层次就是为了给 MEM 阶段喂得上。架构篇和缓存篇是同一条流水线的上下游。

load-use 是前推抄不走的那一拍。从内存读出的数最早在 MEM 阶段末尾才稳定,而下一条若在 EX 就要用,中间至少缺一个周期。编译器若能在空档塞进无关指令,气泡就消失;塞不进,硬件只能插入 stall。记分板(scoreboard)和 Tomasulo 的保留站,都是把"谁在等谁"做成表格,让后面不相关的指令先走。五级教学流水线用前推就能讲完;乱序机器把同一问题放大成几百项的窗口。窗口越大,越能藏延迟,也越给侧信道留下可观察的痕迹。

控制冒险(Control Hazard):分支预测

遇到分支(if-else、for 循环)时,CPU 不知道下一条应该执行哪条指令,因为条件还没计算完:

CMP R1, 0          # 比较 R1 与 0
BEQ label          # 如果等于 0,跳转到 label
ADD R2, R3, R4     # 不跳转则执行
... (label 处的指令)
```

当 BEQ 在 EX 阶段得出结论时,后续指令已经进入流水线了。如果预取的指令是错的,必须冲刷流水线,损失若干个时钟周期。

分支预测(Branch Prediction):CPU 预测分支结果,按预测方向取指,如果预测正确则无损失,预测错误则冲刷。

  • 静态预测:总是预测不跳转(或总是跳转),简单但不准
  • 动态预测:维护分支历史表(Branch History Table),记录每个分支上次的结果,以此预测下一次。现代高性能 CPU 的分支预测精度经常被写成超过 95%。循环和规律分支好猜,间接跳转和数据依赖的分支仍会打脸。猜对时流水线像没停过,猜错时要冲掉已经填进去的指令,损失按流水线深度计。深度越长,赌错越贵。于是预测器越做越重,几乎成了另一台小计算机。乱序执行把后面不相关的指令提到前面填空,看起来像打破了程序顺序,提交时仍按原序写回。程序员看见的是顺序机器,硅片上是为了不让流水线挨饿。契约写在 ISA,实现写在微架构。抽象泄漏定律在这里变成侧信道:实现多做的那些预测,成了可被计时的秘密。

现代分支预测器远比"记住上次结果"复杂,而且分成两条至今仍在竞争的真实技术路线:

一条是 TAGE(TAgged GEometric history length,"带标签的几何历史长度"预测器),由 André Seznec 与 Pierre Michaud 在 2006 年提出。它用多张带标签的表,分别以长短不一(按几何级数递增)的历史片段作索引,让很短和很长的分支历史都能发挥作用。TAGE 及其变体包揽了 2006 至 2016 年四届国际分支预测锦标赛(CBP)的冠军,是公认的学术标杆。

另一条是感知机预测器(Perceptron Predictor),由 Daniel Jiménez 与 Calvin Lin 在 2001 年提出——这才是真正基于神经网络的分支预测器。它为每个分支维护一组权重,把分支历史当作输入向量做点积来判断跳还是不跳;硬件开销随历史长度线性增长(而非指数增长),因而能利用很长的历史。AMD 从 2012 年的 Piledriver 到 Zen 系列都采用了感知机预测器,市场上称之为"Neural Net Prediction"。

得益于这两类算法,现代高性能 CPU 在多数程序上的预测精度超过 95%,规整循环里常常高于 99%。要特别澄清一个常见混淆:被称作"神经网络"的是感知机这条路线,TAGE 并不是神经网络。

结构冒险(Structural Hazard)

多条指令同时需要同一个硬件资源。解决方案是提供足够的资源副本(如独立的指令缓存和数据缓存,避免两者争用同一内存端口)。

超标量与乱序执行

现代处理器把流水线进一步推进到两个方向:

超标量(Superscalar):每个时钟周期同时取指、发射多条指令。现代处理器通常是 4–6 路超标量,理论上每周期完成 4–6 条指令(Instructions Per Cycle,IPC);苹果 2020 年的 M1(Firestorm 核)更把译码宽度做到每周期 8 条指令,而同期 x86 大核约为 4–6 条。

乱序执行(Out-of-Order Execution,OoOE):当一条指令等待数据(内存访问慢)时,不停顿,而是找下游没有依赖关系的指令先执行。Robert Tomasulo 在 1967 年为 IBM System/360 Model 91 设计的 Tomasulo 算法是乱序执行的先驱。

要在不破坏程序语义的前提下乱序,硬件还得做两件事:用寄存器重命名(Register Renaming)消除"名字相同但其实无关"的假依赖;再用重排序缓冲(Reorder Buffer,ROB)记下指令的原始顺序,保证最终按程序顺序"提交"结果。ROB 越大,CPU 能同时"在飞"的指令就越多:Intel Golden Cove(2021 年第 12 代酷睿)的 ROB 有 512 项,苹果 M1 的乱序窗口更接近 600 项量级——因此在内存延迟很高时仍能找到大量独立指令填满执行单元。

乱序执行维持了程序的逻辑语义,对程序员透明,但物理上指令以不同于程序文本的顺序执行。这产生了 Spectre 和 Meltdown 漏洞。

推测执行与安全漏洞

推测执行(Speculative Execution):CPU 提前执行"可能不会执行"的指令(如分支后的指令),如果预测正确,节省了时间;如果错误,撤销结果并重新执行正确路径。推测执行是现代高性能处理器的核心技术之一。

Spectre 与 Meltdown:2018 年 1 月 3 日公开的两组漏洞,利用推测执行和缓存的时序副作用,从一个进程读取另一个进程(甚至操作系统内核)的私有数据。它们由多个团队几乎同时独立发现:Google Project Zero 的 Jann Horn 独立找到了二者,学术界则有 Paul Kocher、Moritz Lipp、Yuval Yarom 等人参与。原计划 1 月 9 日协同披露,因外界猜测升温而提前。

原理:CPU 推测执行期间,即使指令最终被回滚,对缓存的影响(哪些数据被加载进缓存)残留下来。攻击者通过测量特定内存地址的访问延迟(缓存命中 vs 缓存缺失),可以推断出被推测执行的代码处理的数据内容。

这两个漏洞影响了几乎所有过去 20 年生产的高性能处理器。针对 Meltdown 的操作系统补丁——内核页表隔离(KPTI)——在系统调用密集的工作负载上可带来高达约 30% 的性能损失。它们揭示:性能优化(推测执行)与安全隔离存在根本张力,不能只看功能正确性,还必须考虑侧信道。

Meltdown 与 Spectre 不是同一道裂缝。Meltdown 利用乱序把本不该被用户态读取的内核映射值送进缓存,操作系统可以用页表隔离把映射拆开;Spectre 则训练分支预测器,让受害进程自己把秘密推测执行进缓存,补丁更难、也更碎。2018 年 1 月 3 日提前公开之后,芯片与系统软件进入了多年的微码与栅栏修补。性能税写在系统调用和上下文切换上,安全税写在"预测不再免费"。流水线从此不能只按吞吐评分。

流水线深度的权衡

更深的流水线(更多阶段)理论上允许更高的时钟频率(每个阶段工作量更少),但:

  • 分支误预测代价更大:流水线越深,预测错误时需要冲刷的阶段越多
  • 功耗增加:维持深流水线的逻辑更复杂,静态功耗更高

Intel 的 NetBurst 架构正是反面教材。最初的 Pentium 4(Willamette/Northwood 核)已是 20 阶段流水线,到 2004 年的 Prescott 核更加深到 31 阶段,时钟频率冲到 3.8 GHz。代价是分支误预测的惩罚几乎等于流水线深度:Willamette 一次误预测约损失 20 个周期,Prescott 升到约 31 个周期,若再叠加缓存缺失会更糟。结果是高频率换不回等比例的性能,反而不如频率更低的竞争对手,NetBurst 被公认为失败。此后处理器设计回归中等深度的流水线,转而以乱序执行和缓存优化提升性能。

此后主流 x86 大核把流水线收回到十几级,靠更宽的发射、更大的 ROB 和更好的预测器换性能,而不是再把时钟往上堆。ARM 与苹果的大核走了类似的路:译码更宽,窗口更大,阶段数保持克制。教科书上的五级 MIPS 仍是理解冒险的最好骨架,硅片上的真实机器是超标量加乱序的放大版。骨架没过时,过时的是"加深流水线就能等比变快"那句宣传。

跨域连接

  • 机器学习概览:分支预测器是硬件里最早大规模部署的在线学习器:每次分支的实际去向就是标签,权重按点积的符号更新。它对线性可分的历史有效,对异或型模式无能为力——这正是多长度历史表那条路线要补的地方。推论是:越是依赖数据取值的分支,越难被任何历史长度的预测器猜准。
  • 心理语言学加工:人读句子也在推测:遇到结构歧义先按最可能的解析往下走,错了就回头重分析,代价能在阅读时间上直接测出。这与误预测后冲刷流水线是同一种代价结构——猜对几乎免费,猜错要付出重来的全部开销。
  • 概率论:有效周期数等于理想值加上各类冒险的期望惩罚,而误预测的期望惩罚约等于流水线深度乘以误预测率。这个乘积解释了为什么加深流水线必须同步提高预测精度,否则频率涨了、实际性能反而下滑。这也解释了极深流水线为何被放弃:频率收益是线性的,惩罚却随深度同步增长。
  • 因果:被回滚的推测执行在架构状态上"没有发生",却在缓存时序里留下可测量的痕迹。可见"发生过"只能相对于某个可观测集合来定义;侧信道攻击做的事,就是把这个集合扩大到设计者没打算暴露的那一层。修补这类漏洞要么减少可观测量,要么放弃部分推测,两者都得付性能。
  • 产业组织:装配线的吞吐由最慢工位决定,与时钟周期由最长流水段决定同构。工序切得越细,节拍越快,但每次返工要重做的工位也越多——这条乘积算错,就会得到高频率却低性能的设计。

参考文献

  • Patterson, D. & Hennessy, J. Computer Organization and Design: The Hardware/Software Interface. Morgan Kaufmann. (经典教材,详细讲述流水线)
  • Kocher, P. et al. Spectre Attacks: Exploiting Speculative Execution. IEEE S&P, 2019. (Spectre 论文)
  • Lipp, M. et al. Meltdown: Reading Kernel Memory from User Space. USENIX Security, 2018. (Meltdown 论文)
  • Tomasulo, R. An Efficient Algorithm for Exploiting Multiple Arithmetic Units. IBM Journal of R&D 11(1), 1967. (乱序执行的开山之作)
  • Hennessy, J. & Patterson, D. Computer Architecture: A Quantitative Approach. Morgan Kaufmann.
  • Seznec, A. & Michaud, P. A Case for (Partially) Tagged Geometric History Length Branch Prediction. Journal of Instruction-Level Parallelism (JILP), 2006. (TAGE 预测器原始论文)
  • Jiménez, D. & Lin, C. Dynamic Branch Prediction with Perceptrons. HPCA, 2001. (感知机/神经网络分支预测的开创论文)
  • Buchholz, W. (ed.) Planning a Computer System: Project Stretch. McGraw-Hill, 1962. (IBM 7030 Stretch 流水线设计的一手资料)