现代 CPU 设计(Modern CPU Design)
为了了解本章所讨论的概念在实践中如何使用,让我们看一下 Intel 第 12 代核心 Golden Cove 的实现,该核心于 2021 年推出。这个核心作为 Alder Lake 和 Sapphire Rapids 平台中的 P 核使用。图 Goldencove_diag 展示了 Golden Cove 核心的框图。注意,本节只描述单个核心,而不是整个处理器。因此,我们将跳过关于频率、核心数量、L3 缓存、核心互连、内存延迟和带宽的讨论。

Intel Golden Cove 微架构中 CPU 核心的框图。
该核心被分为顺序(in-order)的前端(frontend),用于获取和将 x86 指令译码为 μops9,以及一个 6 路超标量乱序后端(out-of-order backend)。Golden Cove 核心支持 2 路 SMT。它具有 32KB 一级指令缓存(L1 I-cache)和 48KB 一级数据缓存(L1 D-cache)。L1 缓存由统一的 1.25MB(服务器芯片中为 2MB)L2 缓存支持。L1 和 L2 缓存对每个核心私有。在本节末尾,我们还将介绍 TLB 层次结构。
CPU 前端(CPU Frontend)
CPU 前端由从内存获取和译码指令的几个功能单元组成。其主要目的是向 CPU 后端(负责指令的实际执行)提供准备好的指令。
从技术上讲,取指是执行指令的第一个阶段。但一旦程序达到稳定状态,分支预测单元(BPU)就会引导 CPU 前端的工作。这由从 BPU 到指令缓存的箭头指示。BPU 预测所有分支指令的目标,并根据此预测引导下一个指令取指。
BPU 的核心是一个包含 12K 条目的分支目标缓冲区(BTB),包含有关分支及其目标的信息。这些信息被预测算法使用。每个周期,BPU 生成下一个取指地址并将其传递给 CPU 前端。
CPU 前端每个周期从 L1 I-cache 取指 32 字节的 x86 指令。这在两个线程之间共享(如果启用了 SMT),因此每个线程每隔一个周期获得 32 字节。这些是复杂的可变长度 x86 指令。首先,预译码(pre-decode)阶段通过检查块来确定和标记可变指令的边界。在 x86 中,指令长度从 1 到 15 字节不等。该阶段还标识分支指令。预译码阶段将最多 6 条指令(也称为宏指令(macroinstructions))移动到在两个线程之间分割的指令队列(Instruction Queue)。指令队列还支持宏操作融合(macro-op fusion)单元,该单元检测两个宏指令何时可以融合成单个微操作(μop)。这种优化节省了流水线其余部分的带宽。
之后,每个周期从指令队列向译码器单元发送最多六条预译码指令。两个 SMT 线程每个周期交替访问此接口。6 路译码器将复杂的宏指令(Macro-Ops)转换为固定长度的 μops。译码后的 μops 被排队到指令译码队列(IDQ,Instruction Decode Queue),在图中标记为"μop 队列"。
前端的一个主要性能提升特性是 μop 缓存(μop Cache)。此外,人们经常称之为译码流缓冲区(DSB,Decoded Stream Buffer)。其动机是将宏指令到 μops 的转换缓存在一个独立结构中,该结构与 L1 I-cache 并行工作。当 BPU 生成新的取指地址时,μop 缓存也会被检查,以查看 μops 转换是否已经可用。频繁出现的宏指令将命中 μop 缓存,流水线将避免为 32 字节束重复昂贵的预译码和译码操作。μop 缓存每个周期可以提供八个 μops,可以容纳多达 4K 个条目。
某些非常复杂的指令可能需要比译码器能处理的更多的 μops。此类指令的 μops 从微码序列器(MSROM,Microcode Sequencer)提供。此类指令的例子包括对字符串操作、加密、同步等的硬件操作支持。此外,MSROM 保存处理异常情况的微码操作,如分支预测错误(需要流水线冲刷)、浮点辅助(例如当指令操作非规格化浮点值时)等。MSROM 每个周期可以向 IDQ 推送最多 4 个 μops。
指令译码队列(IDQ)提供顺序前端和乱序后端之间的接口。IDQ 按顺序排列 μops,在单线程模式下每个逻辑处理器可以容纳 144 个 μops,在 SMT 活动时每个线程可以容纳 72 个 μops。这是顺序 CPU 前端结束和乱序 CPU 后端开始的地方。
CPU 后端(CPU Backend)
CPU 后端采用 OOO 引擎来执行指令并存储结果。我在图 Goldencove_OOO 中重复了描述 Golden Cove OOO 引擎的部分图表。
OOO 引擎的核心是 512 个条目的重排序缓冲区(ROB)。它有几个目的。首先,它提供寄存器重命名。5 只有 16 个通用整数架构寄存器和 32 个浮点/SIMD 架构寄存器,但物理寄存器的数量要高得多。1 物理寄存器位于一个称为物理寄存器文件(PRF,Physical Register File)的结构中。整数和浮点/SIMD 寄存器有独立的 PRF。从架构可见寄存器到物理寄存器的映射保存在寄存器别名表(RAT,Register Alias Table)中。

Intel Golden Cove 微架构 CPU 后端的框图。
其次,ROB 分配执行资源。当一条指令进入 ROB 时,会分配一个新条目并为其分配资源,主要是执行单元和目标物理寄存器。ROB 每个周期可以分配最多 6 个 μops。
第三,ROB 跟踪推测执行。当指令完成执行时,其状态会更新,并停留在那里直到前面的指令完成。这样做是因为指令必须按程序顺序退休。一旦指令退休,其 ROB 条目被释放,指令结果变得可见。退休阶段比分配阶段更宽:ROB 每个周期可以退休 8 条指令。
有某些操作,处理器以特定方式处理,通常称为习语(idioms),它们不需要执行或执行代价更低。处理器识别此类情况并允许它们比常规指令运行得更快。以下是一些此类情况:
- 清零(Zeroing):要将寄存器赋值为零,编译器通常使用
XOR / PXOR / XORPS / XORPD指令,例如XOR EAX, EAX,编译器更倾向于使用这些指令而不是等效的MOV EAX, 0x0指令,因为 XOR 编码使用的编码字节更少。此类清零习语不像其他常规指令那样执行,而是在 CPU 前端解析,节省了执行资源。该指令之后照常退休。 - 移动消除(Move elimination):类似于前一个,寄存器到寄存器的
mov操作,例如MOV EAX, EBX,以零周期延迟执行。 - NOP 指令:
NOP通常用于填充或对齐目的。它被标记为完成,而不分配到保留站。 - 其他旁路:CPU 架构师还优化了某些算术操作。例如,任何数字乘以 1 总是产生相同的数字。除以 1 同样如此。任何数字乘以 0 总是产生 0 等。一些 CPU 可以在运行时识别此类情况,并以比常规乘法或除法更短的延迟执行它们。
"调度器/保留站"(RS,Scheduler/Reservation Station)是跟踪给定 μop 的所有资源可用性并在其就绪时将 μop 分发到执行端口(execution port)的结构。执行端口是将调度器连接到其执行单元的通路。每个执行端口可以连接到多个执行单元。当指令进入 RS 时,调度器开始跟踪其数据依赖。一旦所有源操作数变为可用,RS 尝试将 μop 分发到空闲的执行端口。RS 的条目4比 ROB 少。它每个周期可以分发最多 6 个 μops。
我在图 Goldencove_BE_LSU 中重复了描述 Golden Cove 执行引擎和加载-存储单元的部分图表。有 12 个执行端口:
- 端口 0、1、5、6 和 10 提供整数(INT)操作,其中一些处理浮点和向量(FP/VEC)操作。
- 端口 2、3 和 11 用于地址生成(AGU)和加载操作。
- 端口 4 和 9 用于存储操作(STD)。
- 端口 7 和 8 用于地址生成。

Intel Golden Cove 微架构执行引擎和加载-存储单元的框图。
需要内存操作的指令由加载-存储单元(Load-Store unit)处理(端口 2、3、11、4、9、7 和 8),我们将在下一节讨论。如果操作不涉及加载或存储数据,则它将被分发到执行引擎(端口 0、1、5、6 和 10)。某些指令可能需要必须在不同执行端口上执行的两个 μops,例如加载和加法。
例如,整数移位(Integer Shift)操作只能转到端口 0 或 6,而浮点除法(Floating-Point Divide)操作只能被分发到端口 0。在调度器必须分发两个需要相同执行端口的操作时,其中一个将不得不被延迟。
FP/VEC 栈执行浮点标量和所有打包(SIMD)操作。例如,端口 0、1 和 5 可以处理以下类型的 ALU 操作:打包整数、打包浮点和标量浮点。整数和向量/FP 寄存器文件分开放置。将值从整数栈移动到 FP/VEC 并反之(例如,转换、提取或插入)的操作会带来额外的惩罚。
加载-存储单元(Load-Store Unit)
加载-存储单元(LSU)负责内存操作。Golden Cove 核心使用端口 2、3 和 11 可以发射最多三次加载(三次 256 位或两次 512 位)。AGU 代表地址生成单元(Address Generation Unit),需要它来访问内存位置。它还可以通过端口 4、9、7 和 8 每个周期发射最多两次存储(两次 256 位或一次 512 位)。STD 代表存储数据(Store Data)。
注意,加载和存储操作都需要 AGU 来执行动态地址计算。例如,在指令 vmovss DWORD PTR [rsi+0x4],xmm0 中,AGU 将负责计算 rsi+0x4,该值将用于从 xmm0 存储数据。
一旦加载或存储离开调度器,LSU 就负责访问数据。加载操作将获取的值保存在寄存器中。存储操作将值从寄存器传输到内存中的位置。LSU 有加载缓冲区(Load Buffer,也称加载队列)和存储缓冲区(Store Buffer,也称存储队列);其大小未公开。2 加载缓冲区和存储缓冲区都在调度器分发时接收操作。
当内存加载请求到来时,LSU 使用虚拟地址查询 L1 缓存,并在 TLB 中查找物理地址转换。这两个操作同时启动。L1 D-cache 的大小为 48KB。如果两个操作都命中,加载将数据传送到整数或浮点寄存器并离开加载缓冲区。类似地,存储将把数据写入数据缓存并退出存储缓冲区。
如果 L1 未命中,硬件会启动对(私有)L2 缓存标签的查询。在查询 L2 缓存时,会分配一个 64 字节宽的填充缓冲区(FB,Fill Buffer)条目,该条目将在缓存行到达后保存它。Golden Cove 核心有 16 个填充缓冲区。作为降低延迟的方式,在与 L2 缓存查找并行时,向 L3 缓存发送推测性查询。此外,如果两次加载访问同一缓存行,它们将命中同一 FB。这两次加载将被"粘合"在一起,只发出一个内存请求。
如果确认 L2 未命中,加载继续等待 L3 缓存的结果,这会带来更高的延迟。从那时起,请求离开核心进入非核心(uncore),这是你在性能分析工具中有时会看到的术语。来自核心的未完成未命中在超级队列(SQ,Super Queue,图中未显示)中被跟踪,可以跟踪最多 48 个非核心请求。在 L3 未命中的情况下,处理器开始设置内存访问。更多细节超出了本章的范围。
当存储修改内存位置时,处理器需要加载完整的缓存行,更改它,然后将其写回内存。如果要写入的地址不在缓存中,它会经历与加载非常类似的机制来将数据带进来。在数据写入缓存层次结构之前,存储无法完成。
当然,对存储操作也做了一些优化。首先,如果我们处理的是修改整个缓存行的存储或多个相邻存储(也称为流式存储(streaming stores)),则不需要先读取数据,因为所有字节都将被覆盖。因此,处理器将尝试合并写入以填满整个缓存行。如果成功,则不需要内存读取操作。
其次,写合并(write combining)使多个存储能够作为一个单元组装并写入缓存层次结构的更远处。因此,如果多个存储修改同一缓存行,只向内存子系统发出一次内存写入。所有这些优化都在存储缓冲区内完成。存储指令将要写入的数据从寄存器复制到存储缓冲区中。从那里它可以写入 L1 缓存,或者可以与到同一缓存行的其他存储合并。存储缓冲区容量有限,因此它只能在一段时间内保存对缓存行的部分写入请求。然而,当数据在存储缓冲区中等待写入时,其他加载指令可以直接从存储缓冲区读取数据(存储到加载转发,store-to-load forwarding)。此外,当存在包含加载所有字节的较旧存储且存储数据已产生并在存储队列中可用时,LSU 支持存储到加载转发。
最后,有些情况下我们可以使用所谓的非时间性(non-temporal)内存访问来改善缓存利用率。如果我们执行部分存储(例如,我们覆盖缓存行中的 8 字节),我们需要先读取缓存行。这个新的缓存行将替换缓存中的另一行。然而,如果我们知道我们不再需要这些数据,那么最好不要在缓存中为该行分配空间。非时间性内存访问是不在缓存中保留取指行并在使用后立即丢弃的特殊 CPU 指令。
在典型的程序执行过程中,可能有数十次内存访问正在进行。在大多数高性能处理器中,加载和存储操作的顺序不必与程序顺序相同,这被称为弱内存模型(weakly ordered memory model)。为了优化目的,处理器可以重排内存读写操作。考虑一种情况,当加载遇到缓存未命中并且必须等待数据从内存到来时。处理器允许后续加载在等待数据的加载之前执行。这使得后来的加载在早期加载之前完成,不会不必要地阻塞执行。这种加载/存储重排序使内存单元能够并行处理多个内存访问,这直接转化为更高的性能。
LSU 动态地重新排序操作,支持加载绕过较旧的加载和加载绕过较旧的非冲突存储。然而,有一些例外。就像通过常规算术指令的依赖关系一样,通过加载和存储也存在内存依赖关系。换句话说,加载可以依赖于较早的存储,反之亦然。首先,存储不能在较旧的加载之前重排:
Load R1, MEM_LOC_X
Store MEM_LOC_X, 0
如果我们允许存储在加载之前执行,那么 R1 寄存器可能从内存位置 MEM_LOC_X 读取错误的值。
另一种有趣的情况是当加载消耗较早存储的数据时:
Store MEM_LOC, 0
Load R1, MEM_LOC
如果加载消耗尚未完成的存储的数据,我们不应该允许加载继续执行。但如果我们还不知道存储的地址呢?在这种情况下,处理器预测加载和存储之间是否会有任何潜在的数据转发,以及重排是否安全。这被称为内存消歧(memory disambiguation)。当加载开始执行时,必须将其与所有较旧的存储进行检查,以确定潜在的存储转发。有四种可能的情况:
- 预测:不依赖;结果:不依赖。这是成功的内存消歧,产生最优性能。
- 预测:依赖;结果:不依赖。在这种情况下,处理器过于保守,不允许加载超过存储。这是一个性能优化的错失机会。
- 预测:不依赖;结果:依赖。这是内存顺序违规(memory order violation)。与分支预测错误类似,处理器必须冲刷流水线、回滚执行并重新开始。代价非常高昂。
- 预测:依赖;结果:依赖。加载和存储之间存在内存依赖,处理器预测正确。没有错失的机会。
值得一提的是,在实际代码中,从存储到加载的转发相当常见。特别是,任何使用读-改-写(read-modify-write)访问其数据结构的代码都可能触发此类问题。由于大的乱序窗口,CPU 可以轻易地尝试同时处理多个读-改-写序列,因此一个序列的读可能在前一个序列的写完成之前发生。[UarchSpecificIssues] 中呈现了一个此类示例。
TLB 层次结构
回想一下 [TLBs] 中讲到的,虚拟到物理地址的转换缓存在 TLB 中。Golden Cove 的 TLB 层次结构如图 GLC_TLB 所示。与常规数据缓存类似,它有两个级别,其中第 1 级有针对指令(ITLB)和数据(DTLB)的独立实例。L1 ITLB 有 256 个普通 4K 页的条目,覆盖 1MB 内存,而 L1 DTLB 有 96 个条目,覆盖 384 KB。

Intel Golden Cove 微架构的 TLB 层次结构。
层次结构的第二级(STLB)缓存指令和数据的转换。它是为 L1 TLB 未命中请求提供服务的更大存储。L2 STLB 可以容纳 2048 个近期数据和指令页面地址转换,总共覆盖 8MB 内存空间。2MB 大页可用的条目较少:L1 ITLB 有 32 个条目,L1 DTLB 有 32 个条目,L2 STLB 只能使用 1024 个条目,这些条目也与常规 4KB 页共享。
如果在 TLB 层次结构中未找到转换,则必须通过"遍历"内核页表从 DRAM 中检索转换。回想一下,页表被构建为子表的基数树,子表的每个条目都持有指向树的下一级的指针。
加速页面遍历过程的关键元素是一组分页结构缓存(Paging-Structure Caches)3,用于缓存页表结构中的热门条目。对于 4 级页表,我们有最低有效的 12 位(11:0)用于页面偏移(不转换),位 47:12 用于页号。虽然 TLB 中的每个条目都是一个完整的单独转换,但分页结构缓存只覆盖上面 3 级(位 47:21)。其思想是减少在 TLB 未命中情况下需要执行的加载数量。例如,没有这些缓存,我们将必须执行 4 次加载,这会增加指令完成的延迟。但借助分页结构缓存,如果我们找到地址第 1 和第 2 级(位 47:30)的转换,我们只需执行剩余的 2 次加载。
Golden Cove 微架构有四个专用页面遍历器,允许它同时处理 4 次页面遍历。在发生 TLB 未命中时,这些硬件单元将向内存子系统发出所需的加载,并用新条目填充 TLB 层次结构。页面遍历器生成的页表加载可以命中 L1、L2 或 L3 缓存(细节未公开)。最后,页面遍历器可以预测未来的 TLB 未命中,并推测性地进行页面遍历,以在实际未命中发生之前更新 TLB 条目。
Golden Cove 规格没有公开如何在两个 SMT 线程之间共享资源。但通常,缓存、TLB 和执行单元完全共享,以改善这些资源的动态利用率。另一方面,用于在主要流水线阶段之间暂存指令的缓冲区要么复制要么分区。这些缓冲区包括 IDQ、ROB、RAT、RS、加载缓冲区和存储缓冲区。PRF 也被复制。
1. 约有 300 个物理通用寄存器(GPR)和类似数量的向量寄存器。实际寄存器数量未公开。 ↩
2. 加载缓冲区和存储缓冲区大小未公开,但人们测量到分别为 192 和 114 个条目。 ↩
3. AMD 的等效项称为页面遍历缓存(Page Walk Caches)。 ↩
4. 人们测量到 RS 中约 200 个条目,但实际条目数未公开。 ↩
5. 重命名必须按程序顺序进行。 ↩
9. 复杂的 CISC 指令被转换为简单的 RISC 微操作,参见 [sec_UOP]。 ↩