Skip to content

Alpha 21264处理器

1998年,Alpha 21264以600 MHz的频率和当时最高的SPECint分数震惊了整个行业。更令人惊叹的是,它的设计团队只有约50人——远少于同代Intel和AMD的处理器团队。21264的设计中充满了精彩的工程权衡,其中最具影响力的是Cluster架构和投机式Load执行。这些创新在25年后的现代处理器中仍然被广泛使用。可以说,21264是第一颗将"投机与并行"的设计哲学全面工业化的处理器——它证明了激进的微架构创新可以击败10倍于己的晶体管规模优势。

从本书的统一视角看——处理器设计的本质是在有限的晶体管预算和功耗约束下,通过投机和并行的层层叠加来逼近指令吞吐率的理论上限——Alpha 21264是这一命题的里程碑式验证。它首次在商业处理器中全面实现了Cluster执行(管理并行的物理约束,参见第 35.0 章)和投机Load执行(对内存依赖的投机,参见第 36.0 章的Store Wait Table前身)。21264的Tournament分支预测器、Line/Way预测、Load命中/缺失预测等技术,展现了"投机"思想在处理器流水线各个环节的系统性应用——每个预测器都在其负责的领域中用少量的面积投资(几KB的历史表)换取了显著的延迟缩减。

Alpha 21264(代号EV6)是DEC(Digital Equipment Corporation)于1998年发布的第三代Alpha处理器,也是Alpha架构中微架构设计最为精巧的一款。21264在0.35 μm工艺下集成了1520万个晶体管,初始频率为466 MHz,后期版本(21264C/EV68)在0.18 μm工艺下达到1.25 GHz。它是90年代末至2000年代初性能最强的通用微处理器之一,在SPECint和SPECfp基准测试中长期占据榜首。

21264之所以在处理器设计史上占有特殊地位,不仅因为它的绝对性能,更因为它在有限的晶体管预算下引入了一系列影响深远的微架构创新:Tournament分支预测器、双Cluster整数执行结构、Line/Way预测、Store Wait Map投机消歧、Load命中/缺失预测等。这些技术中的许多后来被工业界广泛采纳,成为现代处理器设计的标准范式。本章将详细剖析21264的各个微架构组件,理解这些经典设计背后的工程权衡与设计哲学。

Alpha 21264概述

Alpha架构简介

Alpha指令集架构(Alpha ISA)是DEC在1992年推出的64位RISC架构,设计目标是追求极致的时钟频率和指令级并行性。Alpha ISA具有以下几个对微架构友好的特征:

(1)固定长度指令编码。所有Alpha指令均为32位(4字节)固定长度,这使得取指和解码阶段的实现显著简化——不需要复杂的指令长度预解码逻辑,取指单元可以按固定边界切割指令。与x86架构中1–15字节的可变长度指令相比,Alpha的固定长度编码允许在单周期内精确确定指令边界,降低了前端流水线的复杂度和延迟。

(2)Load/Store架构。Alpha采用严格的Load/Store架构:只有LDx(Load)和STx(Store)指令可以访问内存,所有算术和逻辑操作只在寄存器之间进行。这一特性简化了执行单元的设计——ALU不需要内嵌地址计算逻辑,存储器访问流水线与计算流水线可以清晰分离。

(3)大寄存器文件。Alpha ISA定义了32个64位整数寄存器(R0R31,其中R31恒为零)和32个64位浮点寄存器(F0F31,其中F31恒为零)。整数和浮点寄存器文件完全独立,各有自己的重命名映射表和物理寄存器文件。

(4)无条件码(No Condition Codes)。Alpha的条件分支指令直接测试寄存器值(如BEQBNEBLT等),而不依赖标志寄存器(condition code register)。这一设计消除了标志寄存器作为隐式依赖源所带来的指令间串行化问题。在x86架构中,许多ALU指令隐式修改EFLAGS寄存器,导致相邻指令之间产生不必要的数据依赖;Alpha的设计则完全避免了这一问题。

(5)特权架构层(PALcode)。Alpha没有硬连线的异常/中断处理机制,而是通过一层称为PALcode(Privileged Architecture Library)的软件层来处理特权操作。PALcode运行在特殊的特权模式下,可以访问硬件内部状态。这一设计使得操作系统的移植更加灵活——不同操作系统(如OpenVMS、Tru64 UNIX、Windows NT、Linux)可以使用不同的PALcode实现。

(6)弱存储器排序模型。Alpha ISA定义了非常弱的存储器排序模型——在没有显式屏障指令(MBWMB)的情况下,处理器可以自由地重排序Load和Store操作。这种弱排序给微架构提供了极大的优化空间:21264的存储器流水线可以在不遵守程序顺序的前提下投机地执行Load和Store,只要最终结果对软件看来是正确的(通过Store Wait Map等机制保证)。

相比之下,x86架构的强排序模型(TSO, Total Store Order)要求处理器保持Store操作的程序顺序,并禁止Load跨越之前的Store进行重排序。这一约束迫使x86处理器在存储器流水线中加入更多的排序控制逻辑,增加了设计复杂度。Alpha的弱排序模型使得21264的存储器消歧投机成本更低——即使投机失败,也只需要重放违规指令,而不需要全局排序检查。

弱排序模型的代价在于增加了软件的编程复杂度——多线程程序的开发者或编译器必须在适当位置插入MB屏障指令,否则可能观察到违反直觉的内存操作顺序。但对于21264这样追求极致单线程性能的设计,弱排序模型提供的微架构自由度是非常有价值的。

设计提示

Alpha ISA的"微架构友好"设计理念对21264的高频实现至关重要。固定指令长度使得取指单元可以在单周期内完成指令切割;无条件码消除了大量虚假依赖;Load/Store架构使得存储器流水线可以独立设计。这些ISA层面的简化最终转化为更短的流水线关键路径和更高的时钟频率。相比之下,同时代的x86处理器(如Pentium III)需要在前端花费大量逻辑进行指令长度解码和微操作拆分,这些额外工作直接增加了流水线深度。

21264的设计目标

21264的设计始于1994年,由DEC位于马萨诸塞州哈德逊(Hudson, Massachusetts)的工程团队领导,关键设计者包括Richard Kessler、Joel Emer等人。21264的设计目标可以概括为以下几点:

(1)相比21164的性能倍增。21264的前身Alpha 21164(EV5)是一款顺序双发射处理器,尽管时钟频率很高(300–600 MHz),但由于缺乏乱序执行能力,IPC较低(典型值约0.8–1.2)。21264的首要目标是在相近工艺节点下实现性能翻倍。为此,设计团队选择了激进的4-wide乱序超标量架构。

(2)高时钟频率。21264的目标频率为500 MHz以上(在0.35 μm工艺下)。为实现这一目标,设计团队采用了7级流水线(后文将详细分析),并在多个关键路径上采用了投机执行(如Line/Way预测、Load命中预测)来缩短流水级延迟。

(3)晶体管预算控制。在0.35 μm工艺下,芯片面积受限,21264的晶体管数目约为1520万——远大于同时代Intel Pentium II的750万(注意21264为纯64位设计,比Pentium II多出约一倍的晶体管)。在有限的晶体管预算下,21264放弃了片上L2 Cache(这是21164的重要特性),将全部预算用于更大的L1 Cache和更强的乱序引擎。

这一决策是21264设计中最具争议性的选择之一。21164(EV5)集成了一个96 KB的片上L2 Cache,为内存密集型工作负载提供了额外的缓冲层。21264的设计者认为,在有限的晶体管预算下,大容量的L1 Cache(128 KB vs 21164的16 KB I + 16 KB D)加上更强的乱序引擎,比小容量的L1 Cache加上片上L2 Cache的组合更有效。这一判断被后来的性能测试所验证——21264的64 KB L1 I-Cache的缺失率不到1%(21164的8 KB I-Cache缺失率约5%–10%),大容量L1有效地替代了片上L2的部分功能。

(4)灵活的存储器层次。21264提供了128 KB的片上L1 Cache(64 KB I-Cache + 64 KB D-Cache),并通过高速系统总线(EV6 bus)连接片外L2/L3 Cache。这一选择在当时是有争议的——竞争对手如IBM POWER3集成了片上L2 Cache。但21264的大容量L1 Cache有效降低了缺失率,而片外L2的容量可以灵活配置(1 MB至16 MB),在整体性能上并不逊色。

(5)多处理器支持。21264从设计之初就考虑了多处理器(SMP)配置。EV6总线协议支持最多4个21264处理器共享同一系统总线,通过MOESI一致性协议维护Cache一致性。多处理器配置被广泛应用于DEC/Compaq的AlphaServer系列服务器和工作站中,如AlphaServer DS25(2路)和AlphaServer GS1280(最多64路,使用多级互连)。

21264的多处理器支持设计包括以下关键特性:

  • 总线监听(Bus Snooping):每个处理器的Cbox监听系统总线上其他处理器的Cache操作请求,维护Cache一致性。

  • 原子操作:Alpha ISA定义了LDx_L(Load Locked)和STx_C(Store Conditional)指令对,用于实现无锁数据结构和同步原语。21264在硬件中支持这些原子操作——LDx_L将地址标记在一个内部锁定寄存器中,STx_C检查锁定标记是否仍然有效(未被其他处理器的写入打断)。

  • 存储屏障MB(Memory Barrier)和WMB(Write Memory Barrier)指令在21264中强制排空Store Buffer和等待未完成的存储器操作完成,保证屏障前后操作的顺序可见性。

性能分析 1 — 21264与同时代处理器的比较

下表将21264与同时期(1997–1999年)的几款代表性处理器进行对比:

参数Alpha 21264MIPS R10000Intel P6IBM POWER3HP PA-8500
发布年份19981996199519981999
工艺0.35 μm0.35 μm0.35 μm0.25 μm0.25 μm
晶体管数15.2M6.8M7.5M15M140M
发射宽度44344
乱序执行
ROB容量8032401656
L1 I-Cache64 KB32 KB16 KB32 KB512 KB
L1 D-Cache64 KB32 KB16 KB64 KB1 MB
片上L2
频率466–600 MHz200 MHz300 MHz200 MHz440 MHz

可以看到,21264在发射宽度和ROB容量上均领先于同时代的竞争者。80条目的ROB使其能够容纳更多的指令窗口,从而更有效地开发指令级并行性。

值得注意的是HP PA-8500虽然晶体管数量达到1.4亿(是21264的近10倍),但其中绝大部分用于片上1.5 MB的Cache,核心乱序执行引擎的复杂度与21264相当。这说明在同一时代的工艺下,芯片面积的主要消费者是SRAM,而非逻辑晶体管。21264选择将SRAM投入到128 KB的L1 Cache而非更大的片上L2 Cache,是一个深思熟虑的资源分配决策。

21264与21164的代际对比。21264相对于其前身21164(EV5)的改进是全方位的。21164是一款2-wide发射的顺序执行处理器,虽然频率极高(后期版本达到625 MHz),但受限于顺序执行的低IPC。两者的关键差异包括:

  • 执行模型:21164为顺序执行(in-order),IPC约0.8–1.2;21264为乱序执行(out-of-order),IPC约1.5–2.0。这使得21264在相近频率下性能几乎翻倍。

  • 发射宽度:21164为2-wide,21264为4-wide。即使单纯从峰值吞吐量看,21264就有2倍的优势。

  • 分支预测:21164使用简单的2-bit分支预测器,误预测率约8%–12%;21264的Tournament预测器误预测率约4%–6%。在分支密集的整数代码中,这一改进直接转化为性能提升。

  • Cache层次:21164有8 KB L1 I + 8 KB L1 D + 96 KB片上L2;21264有64 KB L1 I + 64 KB L1 D(无片上L2)。21264的L1缺失率远低于21164,在大多数工作负载下弥补了缺少片上L2的劣势。

芯片整体结构

21264的芯片整体结构可以分为五个主要功能区域:取指单元(Ibox)、整数执行单元(Ebox)、浮点执行单元(Fbox)、存储器单元(Mbox)以及外部Cache接口单元(Cbox)。图图 40.1展示了21264的整体流水线结构。

Alpha 21264整体流水线结构。前端包括4级取指/解码/重命名流水线,后端的整数执行、存储器访问和浮点执行分别有独立的流水线深度。
Alpha 21264整体流水线结构。前端包括4级取指/解码/重命名流水线,后端的整数执行、存储器访问和浮点执行分别有独立的流水线深度。

21264的流水线总深度为7级(对于整数指令):

  1. Stage 0——Line预测:使用Line Predictor预测下一个取指行的地址和I-Cache的Way。

  2. Stage 1——I-Cache访问:使用预测的地址和Way访问64 KB I-Cache,仅读取一个Way(非两路都读),节省功耗。

  3. Stage 2——Slot/解码:指令切割、解码,检查分支预测器的预测结果。

  4. Stage 3——Map/重命名:寄存器重命名,分配ROB表项和物理寄存器,将指令写入发射队列。

  5. Stage 4——Issue(发射):从发射队列中选择就绪指令发射到执行单元。

  6. Stage 5——Execute(执行):ALU执行计算。

  7. Stage 6——Write-back(写回):将执行结果写回物理寄存器文件。

对于Load指令,Stage 5和Stage 6被替换为D-Cache访问(需要2–3个周期),因此Load-use延迟为3个周期。对于浮点指令,执行阶段需要4–6个周期(取决于具体操作)。

流水线深度的设计权衡。21264的7级流水线在同时代处理器中属于较浅的设计。相比之下,Intel Pentium Pro(P6微架构)使用了12级流水线,后来的Pentium 4(NetBurst微架构)使用了20级甚至31级流水线。流水线深度直接影响两个关键性能参数:

  • 时钟频率:更深的流水线意味着每级的工作量更少,关键路径更短,可以支持更高的时钟频率。Pentium 4通过极深的流水线实现了当时最高的频率(3.8 GHz),但这导致了高频率低IPC的问题。

  • 分支预测失败惩罚:流水线深度直接决定了分支预测失败的最小恢复周期数。21264的7级流水线对应约7–10周期的惩罚;Pentium 4的20级流水线对应约20个周期的惩罚。更高的惩罚意味着相同误预测率下更大的IPC损失。

21264选择7级流水线,体现了DEC设计团队"IPC优先"的设计哲学——宁可牺牲一些时钟频率上限,也要保持低分支惩罚和高IPC。这与Intel后来"频率优先"(Pentium 4的设计哲学)形成了鲜明对比。历史证明,21264的IPC导向设计在实际工作负载上的性能/功耗比优于Pentium 4的频率导向设计。

各功能区域的面积占比。21264芯片面积的大致分配如下:

  • L1 I-Cache(64 KB SRAM + Tag + 预解码位):约25%

  • L1 D-Cache(64 KB SRAM + Tag):约25%

  • 取指前端(Ibox,包括分支预测器、Line Predictor、解码逻辑):约10%

  • 整数执行(Ebox,包括两个Cluster的PRF和ALU):约15%

  • 浮点执行(Fbox,包括FP PRF和两个FP单元):约8%

  • 存储器单元(Mbox,包括Store Buffer、TLB、消歧逻辑):约7%

  • 外部接口(Cbox,包括MAF、Victim Buffer、总线逻辑):约5%

  • I/O引脚驱动和时钟分配:约5%

可以看到,L1 Cache占据了芯片面积的约50%——这是21264放弃片上L2 Cache的直接体现。将一半的芯片面积用于大容量L1 Cache,使得L1缺失率极低(I-Cache缺失率<<1%、D-Cache缺失率<<5%),从而减少了对片上L2 Cache的需求。

硬件描述 1 — 21264的关键微架构参数

下表汇总了21264的关键微架构参数:

参数
取指宽度4条指令/周期(1个对齐的取指块)
解码/重命名宽度4-wide
发射宽度4(整数)+ 2(浮点)
整数物理寄存器80个(32逻辑 + 48重命名)
浮点物理寄存器72个(32逻辑 + 40重命名)
整数发射队列20条目
浮点发射队列15条目
ROB(In-flight窗口)80条目
整数执行单元4个(2个Cluster,每Cluster 2个)
浮点执行单元2个(FADD + FMUL)
L1 I-Cache64 KB, 2-way set-associative
L1 D-Cache64 KB, 2-way set-associative, 3-cycle延迟
分支预测器Tournament(Local + Global + Choice)
流水线深度7级(整数)

取指令和分支预测

取指单元(Instruction Fetch Unit),在21264的文档中称为Ibox,是整个处理器最精巧的部分之一。21264的取指设计面临一个核心矛盾:64 KB的2-way set-associative I-Cache需要在单周期内完成访问,但在目标频率下,2-way的Tag比较和数据选择(Way selection)无法在单周期内完成。21264的解决方案是引入Line/Way预测机制,通过预测来避免Way选择的延迟。

Line/Way的预测

在传统的set-associative Cache设计中,访问过程需要两个串行步骤:首先用索引(index)读出一组(set)中所有Way的Tag和数据,然后将Tag与地址进行比较,确定命中的Way,最后选择该Way的数据输出。对于2-way cache,这意味着需要同时读出两个Way的数据,经过Tag比较后用一个2:1多路选择器(MUX)选择正确的Way。这个"Tag比较 \to MUX选择"的关键路径在高频设计中可能构成时序瓶颈。

一种常见的解决方案是并行访问(parallel access):同时读出所有Way的数据并同时进行Tag比较,在周期末尾用比较结果选择正确的Way。但这要求同时驱动所有Way的数据读出,功耗较高。另一种方案是串行访问(serial access):先读Tag、确定命中Way后,在下一个周期再读数据,但这使Cache访问延迟增加到2个周期。

21264采用了第三种方案——Way预测(Way Prediction)。其核心思想是:在访问I-Cache之前,预测本次访问将命中哪个Way,然后仅读取预测Way的数据。如果预测正确(绝大多数情况),则在单周期内完成Cache访问;如果预测错误,则在下一个周期重新读取另一个Way的数据(额外花费1个周期惩罚)。

Line Predictor的结构。21264的Line Predictor不仅预测Way,还同时预测下一个取指行的地址(Next-Line Address)。这使得Line Predictor实际上承担了一级分支目标缓冲(BTB)的功能。Line Predictor是一个与I-Cache行一一对应的表格,每个I-Cache行在Line Predictor中有一个对应的条目,包含以下信息:

  • 下一行地址(Next-Line Address):预测该取指行执行完毕后,下一个应该取指的行地址。对于顺序执行的代码,这是当前行的下一个自然地址(PC+16字节,因为每行包含4条指令);对于包含taken分支的代码,这是分支目标所在行的地址。

  • 下一行Way(Next-Line Way):预测下一个取指行位于I-Cache的哪个Way中。这个1-bit的信息消除了Way选择延迟。

  • 分支目标类型信息:标识该行中是否包含分支指令、以及分支的基本类型。

21264的Line/Way预测机制。Line Predictor同时提供下一行地址和Way选择信号,使I-Cache可以在单周期内仅读取一个Way的数据。Tag比较在下一周期进行验证。
21264的Line/Way预测机制。Line Predictor同时提供下一行地址和Way选择信号,使I-Cache可以在单周期内仅读取一个Way的数据。Tag比较在下一周期进行验证。

Line Predictor的工作流程。Line Predictor的工作可以描述为一个自驱动的循环:

  1. 在Stage 0,Line Predictor使用当前取指行的索引查找对应条目,读出预测的下一行地址和Way。

  2. 在Stage 1,使用预测的地址和Way直接读取I-Cache中对应Way的数据(仅读取一个Way),同时也读出两个Way的Tag。

  3. 在Stage 2(Slot/Decode阶段),将读出的Tag与实际地址进行比较,验证Way预测是否正确。如果预测正确,取指流水线正常继续;如果预测错误,丢弃已取回的指令,使用正确的Way重新读取数据(1周期惩罚)。

  4. 同时,Line Predictor的输出(下一行地址和Way)作为下一个周期Stage 0的输入,驱动新一轮取指。

这种自驱动的设计使得取指单元可以在每个周期产生一个新的取指请求,而不需要等待前一个取指完成后才计算下一个地址。Line Predictor实质上充当了一个"取指地址生成器",将地址计算的延迟从关键路径上移除。

Line Predictor的存储组织。Line Predictor的条目数等于I-Cache的行数。21264的64 KB 2-way I-Cache有64KB/(32B×2)=102464\text{KB} / (32\text{B} \times 2) = 1024个组(set),但Line Predictor需要覆盖两个Way中的每一行。因此Line Predictor共有1024×2=20481024 \times 2 = 2048个条目,使用取指行地址的低位和当前Way作为索引。

每个条目包含:

  • 下一行索引(\sim10位):预测的下一取指行在I-Cache中的组索引。

  • 下一行Way(1位):预测的下一取指行所在的Way。

  • 下一行Tag提示(若干位):可选的Tag高位信息,用于加速Tag比较。

  • 分支类型(2–3位):标识该行中的主要分支类型(无分支/条件分支/无条件跳转/函数调用/返回)。

每个条目约15–20位,总存储量约为2048×1836,8642048 \times 18 \approx 36{,}864位,约4.5 KB。Line Predictor的存储量与Tournament分支预测器的3.6 KB接近,两者加起来约占芯片面积的1%左右——相对于它们带来的性能提升(避免Way选择延迟和提高取指效率),这一面积投入是非常值得的。

Line Predictor的更新时机。Line Predictor的更新发生在以下几个时机:

  1. 分支预测器修正时(Stage 2):当Tournament预测器的结果与Line Predictor的预测不一致时,使用Tournament预测器的结果更新对应条目。

  2. 分支实际解析后(Stage 6+):当分支指令在BRU中被解析后,使用实际的分支目标更新Line Predictor。这是最准确的更新来源。

  3. I-Cache填充时:当新的指令行从L2 Cache填充到I-Cache时,Line Predictor为新行创建初始条目。初始的下一行地址默认为顺序下一行(PC+32字节),Way设置为填充的Way。

预测准确率与惩罚。根据DEC的测试数据,Line Predictor的Way预测准确率超过85%。对于整数基准测试(SPECint95),准确率约为90%;对于浮点基准测试(SPECfp95),由于代码的空间局部性更好,准确率更高。Way预测错误的惩罚为1个周期,因此平均影响较小:对于10%的误预测率,取指的平均额外延迟为0.10×1=0.100.10 \times 1 = 0.10个周期,即有效取指延迟从1个周期增加到约1.1个周期。

设计提示

Line/Way预测是21264最具创新性的设计之一。它的核心思想是"用投机换延迟"——通过预测来避免串行的Tag比较和Way选择步骤,将2-way associative Cache的访问延迟缩短到等效direct-mapped Cache的单周期。这一思想后来被广泛应用于更大关联度的Cache设计中。例如,AMD的Zen架构在L1 D-Cache中使用Way预测来将8-way Cache的访问延迟优化到等效direct-mapped的程度;ARM的Cortex-A系列也在多个核心中采用了类似的Way预测技术。从某种意义上说,21264的Line Predictor是现代Way预测技术的先驱。

与传统BTB的区别。Line Predictor与传统的BTB(Branch Target Buffer)有本质区别。传统BTB为每个分支指令维护一个条目,记录该分支的目标地址;BTB使用分支指令的PC进行查找,只有在检测到当前指令是分支指令时才使用BTB的预测结果。而Line Predictor为每个取指行(cache line)维护一个条目,记录的是整行执行完毕后的"下一行"地址——无论这个地址变化是由顺序执行、taken分支还是函数调用引起的。

这种以"行"为粒度的预测方式有几个优势:

  • 索引简单:直接使用当前取指行的行地址作为索引,不需要先识别分支指令。

  • 覆盖范围广:即使一行中有多条分支指令,Line Predictor也只需要记住最终生效的那条分支的目标行。

  • 存储效率高:条目数等于I-Cache的行数,没有额外的容量开销。

但Line Predictor也有局限性:如果同一个取指行中有多条分支指令,且不同执行时刻不同分支生效(taken),则Line Predictor只能记住最近一次的目标行。这种情况下需要依赖更精确的分支预测器(如Tournament预测器)进行修正。

为什么2-way I-Cache需要Way预测

要深入理解Line/Way预测的必要性,需要从高频处理器的时序约束出发进行分析。21264的目标频率为500 MHz以上,在0.35 μm工艺下对应的时钟周期约为2 ns。在这2 ns的时间预算中,I-Cache访问的关键路径包括以下串行步骤:

(1)地址解码与SRAM行选择。使用取指地址的索引位(index bits)对SRAM阵列进行行选择。64 KB的2-way I-Cache,每个Way为32 KB,行大小为32字节(8条Alpha指令),共有32KB/32B=102432\text{KB}/32\text{B} = 1024行。行选择需要10-bit地址解码,驱动字线(wordline)激活对应的SRAM行。这一步骤在0.35 μm工艺下约需0.4–0.5 ns。

(2)SRAM位线感应与数据读出。被选中行的存储单元将数据驱动到位线上,差分感应放大器(sense amplifier)检测位线电压差并输出数据。对于32字节(256位)宽的数据读出,这一步骤约需0.5–0.6 ns。

(3)Tag比较。同时读出两个Way的Tag(约20位物理Tag),与取指地址的高位进行比较。Tag比较器本身较快(约0.2 ns),但需要等待Tag数据从SRAM读出。

(4)Way选择MUX。Tag比较结果驱动一个2:1多路选择器,从两个Way的数据中选择命中Way的数据输出。这个MUX的延迟约为0.2–0.3 ns。

如果采用传统的并行访问方式(同时读出两个Way的数据和Tag),关键路径为:行选择 \to 数据读出 \to Tag比较 \to Way MUX选择。总延迟约为0.5+0.6+0.2+0.3=1.60.5 + 0.6 + 0.2 + 0.3 = 1.6 ns。虽然这个延迟小于2 ns的时钟周期,但还需要为时钟分布偏斜(clock skew)、建立时间(setup time)和流水线寄存器留出约0.3–0.4 ns的余量。这意味着实际可用于逻辑的时间只有约1.6 ns,传统并行访问几乎无法满足时序要求。

更严重的问题在于功耗。并行访问两个Way意味着每次取指都要驱动两个Way的全部256位数据位线,但最终只使用其中一个Way的数据。对于高频处理器,I-Cache的动态功耗与每周期切换的位线数量直接相关。如果每次都读两个Way,I-Cache的数据读出功耗翻倍。在0.35 μm工艺下,21264的功耗预算约为70 W,其中分配给I-Cache的功耗预算有限,翻倍的读出功耗是不可接受的。

Way预测巧妙地解决了这两个问题。通过在Cache访问之前预测命中的Way,处理器可以:

  • 消除Way MUX的延迟:不需要等Tag比较结果来选择Way,因为预测已经确定了应该读哪个Way。数据从SRAM读出后可以直接进入取指缓冲,无需经过MUX。

  • 只读取一个Way:仅激活预测Way的数据SRAM阵列,另一个Way的数据位线保持不活动。这将数据读出功耗降低了约50%。

  • 简化数据路径:单Way的数据读出路径比双Way的MUX选择路径更简单,有利于物理布局和布线。

性能分析 2 — Way预测对I-Cache访问延迟的影响

可以定量比较三种I-Cache访问方式在21264参数下的延迟:

访问方式关键路径典型延迟功耗
并行访问(两Way同时读)数据读 \to Tag比较 \to MUX\sim1.6 ns
串行访问(先Tag后数据)Tag读 \to 比较 \to 数据读\sim2.2 ns(2周期)
Way预测(只读一个Way)数据读(单Way)\sim1.1 ns

Way预测使I-Cache的有效访问延迟降至约1.1 ns,为2 ns的时钟周期留下了充裕的时序余量。同时功耗接近串行访问的低水平。代价仅为预测错误时的1周期惩罚。

Way预测错误的检测与恢复

Way预测虽然在绝大多数情况下是正确的,但预测错误时需要一个高效的恢复机制。21264的Way预测恢复过程分为以下几个步骤:

(1)错误检测。Tag比较在Stage 2(Slot/Decode阶段)完成。比较器将实际命中的Way与Line Predictor预测的Way进行对比。如果两者不一致,产生一个Way Misprediction信号。注意,这里有两种情况需要区分:

  • 预测Way错误但对方Way命中:这是典型的Way预测错误。处理器只需要重新读取正确Way的数据,惩罚为1个周期。

  • 两个Way都不命中(I-Cache Miss):这是Cache缺失,与Way预测无关。处理器需要从L2 Cache或更低层次取指令,延迟远大于1个周期。

(2)流水线气泡插入。Way预测错误检测到后,Stage 2已经使用了错误Way的数据进行解码。这些解码结果必须被丢弃。处理器在Stage 2插入一个1周期的气泡(bubble),同时使用正确的Way信号重新驱动I-Cache数据SRAM的读取。

(3)Line Predictor的更新。Way预测错误后,Line Predictor中对应条目的Way预测位被更新为正确的Way。这是一个简单的1-bit写入操作。同时,如果Line Predictor的下一行地址预测也不正确(例如,错误Way中的指令可能暗示了不同的控制流),下一行地址也需要更新。

(4)正常恢复。重新读取的正确数据在下一个周期进入Decode阶段,流水线恢复正常执行。整个恢复过程只消耗1个周期,对性能影响很小。

Way预测错误的原因分析。Way预测错误主要发生在以下场景:

  • Cache行替换后:当一个I-Cache行因容量或冲突原因被替换(从Way 0移动到Way 1,或反之),Line Predictor中记录的旧Way信息不再正确。第一次访问新位置时会产生Way预测错误,之后Line Predictor被更新,后续访问恢复正确。

  • 上下文切换后:操作系统进行上下文切换时,新进程的代码可能与旧进程的代码占据I-Cache中不同的Way位置。Line Predictor需要重新学习新的Way映射,在学习期间Way预测错误率会暂时升高。

  • 别名冲突:由于Line Predictor使用取指行地址的低位索引,不同的取指行可能映射到同一个Line Predictor条目(aliasing)。当两个交替访问的取指行映射到同一条目但位于不同Way时,Line Predictor会反复被覆盖,导致持续的Way预测错误。

设计提示

21264的Way预测恢复机制设计得非常轻量——仅需1个周期的气泡和1-bit的更新。这种低代价恢复是Way预测方案可行的关键前提。如果恢复代价像分支预测失败那样需要7–10个周期,即使Way预测准确率达到90%,其性能损失也将是不可接受的。Way预测方案的成功依赖于两个条件:高预测准确率(>>85%)和低恢复代价(1周期)。这两个条件缺一不可——前者减少错误发生的频率,后者降低每次错误的影响。

I-Cache的组织结构

21264的I-Cache是一个64 KB、2-way set-associative的Cache,行大小为32字节(可容纳8条32-bit Alpha指令)。每个Way的容量为32 KB,共有1024个Cache组(set)。I-Cache采用虚拟地址索引、物理地址Tag的VIPT方案。

取指块的对齐。21264每周期取指4条指令,对应16字节。取指块(fetch block)必须在16字节边界上对齐——即取指地址的低4位必须为零。由于I-Cache行为32字节,每个Cache行可以包含2个对齐的取指块。Line Predictor预测的是下一个取指的地址(32字节粒度),但在行内的起始位置由分支目标地址的低位决定。

I-Cache的预解码位。21264的I-Cache在每条指令旁边存储了额外的预解码位(pre-decode bits)。当指令从L2 Cache填充到I-Cache时,填充逻辑对每条指令进行初步解码,提取以下信息并存储为预解码位:

  • 指令类型(分支、整数运算、浮点运算、存储器访问等)——用于后续的调度和Slot分配。

  • 是否为分支指令——用于快速识别需要分支预测的指令。

  • 分支类型(条件/无条件、直接/间接)——用于选择预测方式。

  • 目标寄存器类型(整数/浮点)——用于确定使用哪个重命名映射表。

预解码位的引入将部分解码工作从取指流水线的关键路径上移除。在没有预解码的设计中,Stage 2(Decode)需要对原始32-bit指令字进行完整解码,这可能成为时序瓶颈。通过将简单的分类工作前移到Cache填充阶段(这是非关键路径,因为Cache填充的延迟由L2访问主导),取指关键路径上的解码工作被简化,有利于实现高频率。

I-Cache的替换策略。21264的I-Cache使用简单的随机替换策略,而非LRU。在2-way的低关联度下,LRU和随机替换的缺失率差异不大(典型差异小于1%),而随机替换的硬件实现远比LRU简单——只需一个简单的LFSR(线性反馈移位寄存器)生成伪随机选择信号,而LRU需要为每个Cache组维护使用历史位。在高频设计中,每一位额外的状态都可能增加面积和时序压力,随机替换是一个务实的选择。

ASN(Address Space Number)支持。Alpha架构支持地址空间编号(ASN),用于区分不同进程的虚拟地址空间。21264的I-Cache Tag中包含ASN字段,使得上下文切换时不需要刷新整个I-Cache——不同进程的Cache行可以共存于I-Cache中,只要它们的ASN不同就不会产生错误匹配。这一特性显著降低了上下文切换的性能代价。

硬件描述 2 — I-Cache Tag的字段组成

21264 I-Cache的每个Tag条目包含以下字段:

字段宽度(bit)功能
物理Tag\sim20物理地址的高位,用于匹配
ASN8地址空间编号,区分不同进程
有效位(Valid)1标识该行是否有效
ASM位1地址空间匹配位(0=仅匹配当前ASN,1=匹配所有ASN)
总计\sim30每Way

ASM(Address Space Match)位用于标识全局共享的代码页(如操作系统内核代码)。当ASM位为1时,该Cache行匹配任何ASN,不受上下文切换的影响。这避免了内核代码在上下文切换后被不必要地重新加载。

I-Cache的每个Cache行除了Tag外,还包含32字节(256位)的指令数据和若干预解码位。每条指令(4字节)附带约2–4个预解码位,每个32字节的Cache行总共约8–16个预解码位。因此每个Cache行的总存储量约为256+16+30=302256 + 16 + 30 = 302位(数据 + 预解码 + Tag),这还不包括ECC或奇偶校验位。

分支预测器的结构

21264使用了一个Tournament分支预测器——这是处理器历史上最经典的分支预测器设计之一,由Scott McFarling在1993年首次提出,21264是将其成功应用于商用处理器的里程碑。

Tournament预测器的核心思想是:维护两个独立的方向预测器——一个局部预测器(Local Predictor)和一个全局预测器(Global Predictor),再用一个选择器(Choice Predictor/Chooser)在两者之间进行动态选择。对于每次分支预测,如果局部预测器在最近的历史中表现更好,则选择局部预测器的结果;反之则选择全局预测器的结果。

局部预测器(Local Predictor)。局部预测器基于每条分支指令自身的历史模式进行预测。它由两个结构组成:

  • 局部历史表(Local History Table, LHT):1024个条目,每个条目包含一个10-bit的局部历史寄存器(Local History Register, LHR)。LHT使用分支PC的低10位进行索引。每个LHR记录了对应分支最近10次的方向历史(0=not-taken, 1=taken)。

  • 局部预测表(Local Prediction Table, LPT):1024个条目,每个条目包含一个3-bit的饱和计数器。LPT使用LHR的值(10-bit历史模式)作为索引进行查找。3-bit饱和计数器的最高位作为预测方向:值4\geq 4预测taken,值<4< 4预测not-taken。

局部预测器的工作流程是:用分支PC索引LHT读出10-bit局部历史,然后用这10-bit历史索引LPT读出3-bit计数器,取计数器最高位作为局部预测结果。这种两级结构使得局部预测器能够捕获每条分支指令特有的方向模式——例如,一个循环分支如果固定执行8次后退出,则它的局部历史呈现"TTTTTTTN"的重复模式,10-bit的历史窗口足以学习到这一模式。

全局预测器(Global Predictor)。全局预测器基于所有分支指令的全局历史进行预测。它由一个单一的全局历史寄存器(Global History Register, GHR)和一个全局预测表(Global Prediction Table, GPT)组成:

  • 全局历史寄存器(GHR):一个12-bit的移位寄存器,记录最近12条分支指令的方向(0=not-taken, 1=taken)。每当一条分支被解析时,其实际方向被移入GHR的最低位。

  • 全局预测表(GPT):4096个条目(2122^{12}),每个条目包含一个2-bit的饱和计数器。GPT使用GHR的12-bit值进行索引。

全局预测器的优势在于捕获分支相关性(branch correlation)——即一条分支的方向可能取决于之前几条分支的方向。例如,在"if (A) ... if (A && B) ..."这样的代码中,第二个分支的方向与第一个分支高度相关。全局预测器通过12-bit的全局历史可以捕获最多12条分支之间的相关模式。

选择器(Choice Predictor)。选择器决定使用局部预测器还是全局预测器的结果。选择器本身也是一个4096条目的表,每个条目是一个2-bit饱和计数器,使用GHR的12-bit值进行索引(与全局预测器共享索引)。

选择器的训练规则如下:

  • 如果局部预测正确而全局预测错误:计数器向"选择局部"方向递增。

  • 如果全局预测正确而局部预测错误:计数器向"选择全局"方向递减。

  • 如果两者预测结果相同(无论对错):计数器不更新。

Alpha 21264的Tournament分支预测器结构。局部预测器(左)使用每分支的局部历史模式进行预测;全局预测器(右)使用全局分支历史进行预测;选择器(中)动态选择两者中表现更好的结果。
Alpha 21264的Tournament分支预测器结构。局部预测器(左)使用每分支的局部历史模式进行预测;全局预测器(右)使用全局分支历史进行预测;选择器(中)动态选择两者中表现更好的结果。

Tournament预测器的存储预算。21264 Tournament预测器的总存储量可以计算如下:

  • 局部历史表(LHT):1024×10=10,2401024 \times 10 = 10{,}240 bits

  • 局部预测表(LPT):1024×3=3,0721024 \times 3 = 3{,}072 bits

  • 全局预测表(GPT):4096×2=8,1924096 \times 2 = 8{,}192 bits

  • 选择表(Choice):4096×2=8,1924096 \times 2 = 8{,}192 bits

  • 全局历史寄存器(GHR):1212 bits

总计约29,70829{,}708 bits,约合3.6 KB。在0.35 μm工艺下,这一存储量的面积开销是可以接受的。

硬件描述 3 — Tournament预测器存储量的详细计算

深入分析各组件的存储量及其设计选择的理由:

局部历史表(LHT)——10,240 bits。1024个条目×\times10-bit历史 = 10,240 bits。为什么选择1024个条目和10-bit历史?1024个条目使用分支PC的低10位索引,在典型的工作集中(数百到数千条不同的分支指令),大部分分支可以映射到独立的LHT条目。10-bit历史窗口意味着可以记录最近10次分支方向,这足以捕获大多数循环模式——例如,一个执行NN次的循环(N10N \leq 10)的分支模式可以被10-bit历史完全记录。如果使用更短的历史(如6-bit),则无法区分"执行7次后退出"和"执行8次后退出"的循环。

局部预测表(LPT)——3,072 bits。1024个条目×\times3-bit计数器 = 3,072 bits。注意LPT的条目数是1024而非210=10242^{10} = 1024——这意味着LPT使用LHR的10-bit值直接索引,条目数等于所有可能的10-bit历史模式数量。3-bit饱和计数器比2-bit计数器提供了更强的"惯性"——需要更多次反方向才能翻转预测,这对于偶尔有例外的规律性模式(如循环出口分支)更加鲁棒。

全局预测表(GPT)——8,192 bits。4096个条目×\times2-bit计数器 = 8,192 bits。GPT使用12-bit GHR索引,212=40962^{12} = 4096个条目。全局预测器使用2-bit而非3-bit计数器,是因为全局预测依赖的是分支相关性模式——这些模式变化较快,不需要过强的惯性。

选择表(Choice)——8,192 bits。4096个条目×\times2-bit计数器 = 8,192 bits。选择器与GPT共享索引(12-bit GHR值),这是一个关键的设计选择——它意味着选择器的决策不仅基于"哪个预测器对这条特定分支更好",还基于"在当前全局历史上下文中哪个预测器更好"。这使得选择器的决策更加精细。

组件条目数每条目位数总位数占比
局部历史表(LHT)10241010,24034.4%
局部预测表(LPT)102433,07210.3%
全局预测表(GPT)409628,19227.6%
选择表(Choice)409628,19227.6%
全局历史寄存器(GHR)11212<<0.1%
总计29,708100%

可以看到,局部预测器(LHT+LPT)占用了约44.7%的存储,全局预测器(GPT)和选择器(Choice)各占约27.6%。这种分配反映了设计者的判断:局部历史模式需要更大的存储来记录每条分支的个体行为,而全局历史和选择只需要较少的存储。

值得注意的是,3.6 KB的总存储量在今天看来微不足道,但在1998年的0.35 μm工艺下,这些SRAM单元占据了可观的芯片面积。21264的设计者在预测精度和存储开销之间进行了精确的权衡——增加存储量(如使用更长的全局历史或更大的LHT)可以提升预测精度,但面积增长会挤占其他关键组件(如发射队列、ROB等)的预算。

预测精度分析。根据21264设计论文的报告,Tournament预测器在SPECint95基准测试上的平均误预测率约为4%–6%,在SPECfp95上约为1%–3%。选择器的引入使得整体精度优于单独使用局部预测器或全局预测器中的任何一个——在某些程序中局部预测器表现更好(如循环密集的科学计算代码),而在另一些程序中全局预测器表现更好(如分支相关性强的控制流代码)。选择器能够自适应地在两者之间切换,获得两者的最佳表现。

局部预测器擅长的场景。局部预测器的优势在于捕获单条分支的固有行为模式。典型的受益场景包括:

  • 固定迭代次数的循环:例如一个执行5次后退出的for循环,其回边分支呈现"TTTTN"的重复模式。10-bit的局部历史可以完整记录这一模式(5位足够),LPT中对应的计数器会针对每种历史前缀学习到正确的预测。

  • 数据无关的条件分支:某些条件分支的方向主要由程序结构而非数据值决定。例如,编译器生成的循环边界检查通常呈现规律性的taken/not-taken模式。

  • 交替模式:某些分支呈现"TNTN..."的交替模式(如if (i % 2 == 0)形式的代码),局部预测器通过2-bit的历史就能准确预测。

全局预测器擅长的场景。全局预测器的优势在于捕获不同分支之间的相关性(correlation)。典型受益场景包括:

  • 关联条件分支:如if (x > 0) {...} if (x > 0 && y > 0) {...},第二个分支的方向与第一个分支高度相关——如果第一个分支not-taken(x0x \leq 0),第二个分支一定not-taken。全局预测器通过12-bit的全局历史可以发现这种跨分支的相关性。

  • 路径相关分支:某些分支的方向取决于程序到达该分支的执行路径。例如,一个函数可能从多个不同的调用点被调用,在不同调用点进入时,函数内的分支可能呈现不同的行为模式。全局历史隐式地编码了执行路径信息。

  • 数据依赖分支:当分支方向由来自不同数据源的多次比较决定时,全局历史可以捕获这些比较结果之间的统计关联。

案例研究 1 — Tournament预测器的历史影响

21264的Tournament预测器对后续处理器设计产生了深远影响。它证明了一个重要的设计原则:在分支预测中,没有单一的最佳策略——不同程序、不同代码段需要不同的预测方法。通过动态选择机制在多个预测器之间切换,可以在不同工作负载之间获得鲁棒的预测精度。

这一原则后来催生了更多的"混合预测器"(hybrid predictor)设计:

  • Intel Pentium M(2003年)采用了类似的两级选择方案,结合局部和全局预测器。

  • TAGE预测器(TAgged GEometric, 2006年由André Seznec提出)将混合思想推向极致:使用多个不同历史长度的全局预测表,通过Tag匹配选择最合适的预测来源。TAGE可以被视为Tournament思想的推广——从"两个预测器中选一个"扩展为"多个预测器中选最佳的一个"。

  • 现代处理器(如AMD Zen系列、Apple M系列)普遍采用TAGE变体作为主预测器,其设计思想可以追溯到21264 Tournament预测器的开创性工作。

案例研究 2 — Tournament预测器的工作示例

以一个具体的代码片段来说明Tournament预测器的工作过程:

    // C代码片段
    for (int i = 0; i < 100; i++) {    // 分支B1: 循环回边
        if (data[i] > threshold) {     // 分支B2: 数据相关
            count++;
        }
        if (i % 2 == 0) {             // 分支B3: 规律模式
            even_sum += data[i];
        }
    }

分支B1(循环回边)——局部预测器优势。B1在前99次为taken(循环继续),最后1次为not-taken(循环退出)。局部历史呈现"TTTTTTTTT..."模式。局部预测器可以准确学习这一模式,在前99次预测taken、最后1次可能预测错误(误预测率约1%)。全局预测器无法利用B2、B3的全局历史来改善B1的预测。选择器应该倾向于选择局部预测器。

分支B2(数据相关)——全局预测器可能占优。B2的方向取决于data[i]的值,这看起来是随机的(取决于数据),局部预测器和全局预测器可能表现相当。但如果data数组的值与之前分支的方向存在统计相关性(如B1已知taken时B2更可能taken),全局预测器可以捕获这种相关性。

分支B3(规律模式)——局部预测器明显优势。B3呈现严格的"TNTN..."交替模式。2-bit的局部历史即可完美预测这一模式,局部预测器准确率接近100%。全局预测器对这种单分支固有模式的预测能力较弱。选择器将学习到在B3上应该信任局部预测器。

这个示例说明了Tournament预测器的价值:对于三个不同特征的分支,选择器能够针对每个分支动态选择表现更好的预测器,实现整体最优的预测精度。单独使用局部预测器虽然在B1和B3上表现好,但可能在B2上表现差;单独使用全局预测器在B2上可能更好,但在B1和B3上浪费了局部历史信息。Tournament预测器通过选择器"两全其美"。

分支预测器的训练与更新。21264的分支预测器在分支指令被实际执行并解析后进行更新。更新过程包括:

  1. 更新局部历史:将分支的实际方向移入对应LHR条目的移位寄存器。

  2. 更新局部预测表:根据实际方向递增或递减LPT中对应3-bit计数器。

  3. 更新全局历史:将分支的实际方向移入GHR。

  4. 更新全局预测表:根据实际方向递增或递减GPT中对应2-bit计数器。

  5. 更新选择器:仅当局部预测和全局预测给出不同结果时,根据哪个正确来调整选择器计数器。

分支预测的时序安排。在21264的7级流水线中,分支预测器的查找发生在Stage 0和Stage 1之间。Line Predictor在Stage 0提供下一行地址,这个地址同时被送入I-Cache和分支预测器。分支预测器在Stage 1进行查找,在Stage 2(Decode阶段)输出预测方向,决定下一个取指的地址是沿着taken路径还是not-taken路径。如果预测方向与Line Predictor预测的下一行地址不一致,处理器需要使用分支预测器的结果修正取指方向,这会引入1个周期的气泡(bubble)。

分支预测的延迟约束。Tournament预测器的查找延迟是取指关键路径上的重要组成部分。预测器的关键路径可以分解为:

  1. 局部预测器:LHT查找(使用PC索引,约0.3 ns)\to LPT查找(使用LHR值索引,约0.3 ns)\to 读出3-bit计数器并取MSB——总延迟约0.7 ns。

  2. 全局预测器:GHR值已经可用 \to GPT查找(使用GHR索引,约0.4 ns)\to 读出2-bit计数器——总延迟约0.5 ns。

  3. 选择器:Choice表查找(与GPT并行,约0.4 ns)\to 根据选择器结果从局部和全局预测中选择——MUX延迟约0.1 ns。

局部预测器是关键路径上最慢的组件(因为需要两次串行查找:先LHT后LPT)。整个Tournament预测器的关键路径约0.8 ns,加上取指地址生成和I-Cache索引的时间,需要在约2 ns的时钟周期内完成。这一时序约束限制了LHT和LPT的容量——更大的表格需要更长的查找延迟。

局部预测器的时序优化。21264可能采用了以下技术来缩短局部预测器的关键路径:

  • LHT和LPT的流水线化:将LHT查找和LPT查找分配到两个连续的流水级中——LHT在Stage 0结束时完成查找,LPT在Stage 1开始时使用LHR值进行查找。这使得局部预测器的结果在Stage 2可用,与全局预测器对齐。

  • LPT的提前索引:LPT可以在LHT查找的同时进行预索引(speculative indexing),使用预测的LHR值开始查找,然后在LHT结果确认后选择正确的输出。

硬件描述 4 — 全局历史的推测更新问题

全局历史寄存器(GHR)的更新面临一个微妙的时序问题。在分支实际解析(Stage 5–6)之前,GHR需要在预测时(Stage 1–2)就被更新——否则后续的分支预测无法使用最新的全局历史。但预测时更新GHR使用的是预测方向而非实际方向,这意味着GHR中包含推测性的信息。

当分支预测失败时,GHR需要被恢复到正确的状态。21264采用了一种称为GHR检查点的机制:在每条分支预测时保存GHR的快照,预测失败时用快照恢复GHR,然后将正确的方向写入恢复后的GHR。这种检查点机制的存储开销较小——每个检查点只需12位(GHR的宽度),但管理逻辑与ROB的检查点管理类似。

具体地,GHR检查点的管理过程如下:

  1. 检查点创建:每当一条分支指令在Stage 2(Decode)被识别时,当前GHR的12-bit值被复制到一个检查点寄存器中。检查点与该分支在ROB中的条目关联。

  2. 推测更新:GHR被立即更新——预测的分支方向被移入GHR的最低位,其他位左移一位。后续的分支预测使用这个推测更新后的GHR进行查找。

  3. 正确解析:当分支在BRU中被解析时,如果预测正确,对应的检查点被释放(不再需要)。

  4. 预测失败恢复:如果预测错误,GHR被恢复为该分支对应的检查点值,然后将正确的方向移入恢复后的GHR。这确保了GHR在恢复点之后反映的是实际发生的分支方向,而非预测方向。

GHR检查点的数量等于同时在飞的最大未解析分支数量。21264支持约4–8个同时在飞的分支(与分支掩码位数一致),因此需要相同数量的GHR检查点。8×12=968 \times 12 = 96 bits的总存储量微不足道。

同时,局部历史表(LHT)不使用检查点——LHT的更新是在分支实际解析后才进行的(非推测性更新),因此不需要恢复机制。这简化了局部预测器的管理逻辑。

这个问题在现代处理器中变得更加复杂:随着GHR长度的增加(现代TAGE预测器使用数百位的全局历史),GHR检查点的存储开销和恢复逻辑也相应增大。一些现代设计采用"分段历史"(segmented history)或"压缩历史"(folded history)技术来降低开销。

返回地址栈

除了方向预测器(Tournament预测器)和目标预测器(Line Predictor)之外,21264还配备了一个返回地址栈(Return Address Stack, RAS),专门用于预测函数返回指令(RET,在Alpha ISA中为JMPR26寄存器指向的地址)的目标地址。

RAS的必要性。函数返回指令的目标地址是一个间接地址——它来自链接寄存器(R26),而R26的值在不同的调用点可能不同。Line Predictor无法准确预测这种间接跳转,因为同一个RET指令每次执行时的目标地址可能不同。RAS利用了函数调用/返回的后进先出(LIFO)特性:最近一次函数调用的返回地址应该是下一次返回指令的目标。

RAS的结构。21264的RAS是一个深度为32的硬件栈。每当检测到一条函数调用指令BSRJSR)时,调用指令的下一条指令地址(PC+4)被压入RAS。每当检测到一条函数返回指令时,从RAS栈顶弹出一个地址作为返回目标的预测值。

RAS的推测性操作与恢复。与GHR类似,RAS也面临推测性操作的问题。在分支预测失败时,推测路径上可能已经压入或弹出了若干RAS条目,导致RAS状态不正确。21264通过以下机制管理RAS的推测状态:

  • RAS指针检查点:在每个分支预测点保存当前RAS栈顶指针。当分支预测失败时,恢复栈顶指针到检查点状态。注意,这只恢复了指针位置,栈中的数据内容可能已被覆盖。

  • 不破坏性弹出:RAS弹出操作仅移动栈顶指针,不清除栈中的数据。这使得指针恢复后,栈中的历史数据仍然有效(前提是没有被新的压入操作覆盖)。

RAS的32层深度对于大多数程序的调用深度已经足够。当调用深度超过32时,最老的返回地址被覆盖,对应的返回预测将失败。在实际程序中(尤其是非递归程序),RAS溢出是罕见事件。

Line Predictor与分支预测器的协同

Line Predictor和Tournament分支预测器在21264的取指单元中协同工作,但它们的预测可能产生冲突。理解两者的交互逻辑对于理解21264的取指性能至关重要。

预测的时序关系。Line Predictor在Stage 0提供下一行地址预测,这个预测在Stage 1驱动I-Cache访问。与此同时,Tournament分支预测器在Stage 1进行查找,在Stage 2输出预测结果。因此,Line Predictor的预测比Tournament预测器早一个周期可用。

这种时序差异导致了一个潜在冲突:Line Predictor在Stage 0已经决定了下一个取指地址,但Tournament预测器要到Stage 2才能确认方向预测是否与Line Predictor的预测一致。如果两者不一致,处理器需要根据Tournament预测器的结果修正取指方向。

三种情况的处理。

  1. Line Predictor和Tournament预测器一致:取指流水线正常推进,无惩罚。这是最常见的情况。

  2. Line Predictor和Tournament预测器不一致:Tournament预测器被视为更准确的预测源(因为它使用了更丰富的历史信息)。取指单元在Stage 2使用Tournament预测器的结果重定向前端,丢弃Stage 0和Stage 1中基于Line Predictor预测已经开始的取指工作。惩罚为1个周期的气泡。同时,Line Predictor中对应条目被更新为Tournament预测器的结果,以提高未来的一致性。

  3. 分支实际执行后发现预测错误:这是真正的分支预测失败(mispredict),无论Line Predictor和Tournament预测器是否一致。恢复惩罚为7–10个周期,包括错误路径的清理和正确路径的取指重启。

Line Predictor的自训练。Line Predictor的训练数据来自两个源:一是Tournament分支预测器的修正(在Stage 2),二是分支实际执行后的反馈(在Stage 6之后)。通过持续的训练,Line Predictor逐渐学习到每个取指行最可能的后继行地址和Way,使得大部分取指请求可以在Stage 0就获得正确的预测,无需等待Tournament预测器的结果。

在稳态执行中(程序的热点代码区域),Line Predictor的准确率非常高(>>90%),大部分取指周期中Line Predictor和Tournament预测器的结果一致,取指流水线可以持续地每周期产生4条新指令。只有在控制流发生变化(新的分支模式出现)或Cache内容变化(I-Cache替换、上下文切换)时,才会出现较高频率的预测不一致。

间接跳转的处理。Alpha ISA中的间接跳转指令(JMPJSRRETJSR_COROUTINE)的目标地址来自寄存器值,无法由Tournament方向预测器处理。21264对间接跳转使用以下策略:

  • 函数返回RET):使用返回地址栈(RAS)预测目标地址,准确率通常>>95%。

  • 间接调用JSR通过寄存器):Line Predictor记住最近一次的目标地址。如果间接调用的目标是固定的(如虚函数表中的固定方法),Line Predictor可以在学习后准确预测。如果目标变化频繁(如解释器中的dispatch table),预测准确率较低。

  • 间接跳转JMP通过寄存器):同样依赖Line Predictor的记忆。对于switch-case语句编译生成的间接跳转表,Line Predictor的效果取决于case的分布模式。

间接跳转(非返回类型)是21264分支预测中的主要弱点。后来的处理器(如Intel Haswell及后续)引入了专门的间接分支目标预测器(Indirect Target Array, ITA),使用全局历史来区分同一间接跳转指令的不同目标。21264没有这种专门结构,对于间接跳转频繁的代码(如面向对象程序中的虚函数调用、解释器的字节码dispatch),性能会受到影响。

寄存器重命名与发射

21264的寄存器重命名和发射机制是其乱序执行引擎的核心。21264采用了物理寄存器文件(Physical Register File, PRF)方案进行寄存器重命名,通过一个独立的寄存器映射表(Register Map Table)维护逻辑寄存器到物理寄存器的映射关系。

寄存器映射表

整数寄存器重命名。21264拥有80个64位整数物理寄存器。Alpha ISA定义了32个逻辑整数寄存器,因此有48个额外的物理寄存器可用于重命名。整数寄存器映射表(Integer Map Table)有32个条目,每个条目存储一个7-bit的物理寄存器编号(log280=7\lceil\log_2 80\rceil = 7)。

浮点寄存器重命名。21264拥有72个64位浮点物理寄存器。Alpha ISA定义了32个逻辑浮点寄存器,因此有40个额外的物理寄存器可用于重命名。浮点寄存器映射表(Floating-point Map Table)同样有32个条目,每个条目存储一个7-bit的物理寄存器编号。

重命名过程。21264的重命名宽度为4-wide,即每周期最多将4条指令进行寄存器重命名。对于每条指令,重命名过程包括以下步骤:

  1. 读取源操作数映射:在映射表中查找每个源逻辑寄存器对应的物理寄存器编号。由于4条指令可能共有8个源操作数,映射表需要提供8个读端口(或通过多个副本来减少端口需求)。

  2. 分配目标物理寄存器:从空闲列表(free list)中为每条有目标寄存器的指令分配一个新的物理寄存器。每周期最多分配4个物理寄存器。

  3. 更新映射表:将目标逻辑寄存器的映射更新为新分配的物理寄存器编号。

  4. 处理同周期内的依赖:如果4条指令中存在先写后读(WAR/RAW)依赖——例如第1条指令写R5,第3条指令读R5——则第3条指令的源操作数应使用第1条指令分配的新物理寄存器,而非映射表中原来的旧映射。这种同周期内的依赖检测和转发需要额外的比较逻辑。

旧映射的保存。21264在重命名时将每条指令覆盖的旧映射(old physical register)记录在ROB条目中。当指令提交时,旧映射对应的物理寄存器被释放回空闲列表(因为此时已经没有指令需要读取旧值)。当分支预测失败时,21264使用WALK恢复将映射表恢复到分支点的状态。

空闲列表管理

空闲列表(Free List)是寄存器重命名机制的关键组件,它跟踪哪些物理寄存器当前未被使用、可以分配给新的指令。21264的空闲列表管理采用了循环FIFO方案。

整数空闲列表。80个整数物理寄存器中,初始时32个被分配给逻辑寄存器的已提交映射,48个进入空闲列表。空闲列表实现为一个循环缓冲区:

  • 分配(Allocate):重命名阶段每周期最多从空闲列表头部取出4个物理寄存器编号,分配给4条新指令的目标寄存器。

  • 释放(Free):退休阶段每周期最多将11个旧映射的物理寄存器编号推入空闲列表尾部(21264的退休宽度为11)。

  • 空闲列表耗尽:当空闲列表中的可用物理寄存器数量不足以满足当前周期的分配需求时,前端暂停(stall),停止解码和重命名,直到退休阶段释放出足够的物理寄存器。

空闲列表与分支恢复。分支预测失败时,错误路径上分配的物理寄存器需要被回收到空闲列表。21264通过WALK恢复机制实现这一过程:从ROB中逆序遍历错误路径上的指令,将每条指令分配的新物理寄存器归还空闲列表,同时恢复映射表中的旧映射。这一过程每周期可以恢复多个条目,但总恢复时间与错误路径上的指令数成正比。

浮点空闲列表。浮点空闲列表的管理方式与整数空闲列表完全相同,只是规模不同——72个浮点物理寄存器中,40个可用于重命名。

映射表的检查点与恢复

21264在处理分支预测失败恢复时,需要将寄存器映射表恢复到分支指令处的状态。21264采用的是WALK恢复方案,而非某些处理器(如MIPS R10000)采用的完全检查点方案。

WALK恢复的工作过程。WALK恢复利用ROB中保存的旧映射信息,逆序撤销错误路径上所有指令的重命名操作:

  1. 从ROB尾指针开始,逆序遍历到分支指令的位置。

  2. 对于每条被遍历的指令,将其ROB条目中保存的旧物理寄存器编号写回映射表的对应逻辑寄存器条目。

  3. 同时将该指令分配的新物理寄存器归还空闲列表。

  4. 遍历完成后,映射表恢复到分支指令处的精确状态。

WALK恢复的延迟。WALK恢复的时间与需要撤销的指令数成正比。在最坏情况下,如果分支预测失败发生在ROB几乎满的时候,可能需要撤销数十条指令,恢复延迟可能达到10–20个周期。但在实际情况中,由于21264的流水线深度只有7级,从分支进入流水线到解析通常只经过约5个周期,错误路径上的指令数量通常不超过4×5=204 \times 5 = 20条(4-wide取指×\times5个周期)。

与检查点方案的比较。完全检查点方案(如MIPS R10000使用的方案)在每个分支指令处保存映射表的完整快照。恢复时直接加载对应检查点,延迟为恒定的1个周期。但检查点方案的代价是存储——每个检查点需要32×7=22432 \times 7 = 224 bits(32个逻辑寄存器×\times7-bit物理寄存器编号),如果支持NN个同时在飞的分支(NN个检查点),总存储为224N224N bits。21264选择WALK恢复方案,牺牲了恢复速度(可变延迟而非固定1周期),换取了节省检查点存储的面积。

设计提示

21264选择WALK恢复而非检查点恢复,反映了一个重要的工程权衡。在21264的设计参数下(80条目ROB、约4%–6%的分支误预测率),分支预测失败是一个相对不频繁的事件。WALK恢复的可变延迟虽然在最坏情况下较长,但由于分支预测精度高,平均恢复延迟是可以接受的。同时,省下的检查点存储面积可以用于扩大发射队列或其他性能关键的结构。在后来的处理器设计中,随着流水线深度加深和指令窗口增大,检查点方案的恢复速度优势变得更加重要——例如MIPS R10000和Intel的某些微架构采用了检查点方案。

硬件描述 5 — 21264的寄存器重命名端口需求

21264每周期对4条指令进行重命名,这对映射表的端口需求如下:

操作整数映射表浮点映射表
读端口(源操作数查找)8(4条指令×\times2个源)8
写端口(目标映射更新)4(4条指令×\times1个目标)4
备份读端口(旧映射保存)44

如此高的端口需求使得映射表成为面积和时序的关键瓶颈之一。21264通过维护映射表的多个副本来降低单个表的端口需求:整数映射表被复制为两份,每份服务于一个整数Cluster(后文将详述Cluster结构)。这种复制策略以面积换取时序——虽然总存储量翻倍,但每个副本的读端口数量减半,显著降低了单个映射表的面积和访问延迟。

重命名窗口大小的影响。21264的80个整数物理寄存器中,32个对应逻辑寄存器的已提交映射,48个可用于重命名。这意味着在任意时刻,最多有48条指令可以处于"已重命名但尚未提交"的状态(就整数目标寄存器而言)。实际上,由于ROB的容量也是80条目,且并非每条指令都写整数寄存器,空闲物理寄存器通常不会成为最严格的瓶颈——ROB容量和发射队列容量往往先被用尽。

但在某些高ILP的代码段中,物理寄存器可能成为限制因素。考虑一段代码,其中每条指令都写一个不同的整数寄存器:在ROB填满80条目之前,48个重命名寄存器就会全部用尽,迫使前端暂停分配。21264的设计者在物理寄存器数量和ROB容量之间取得了平衡——48个重命名寄存器对于大多数程序来说足够,同时避免了更大物理寄存器文件带来的面积和访问延迟增加。

发射队列

21264使用分离的发射队列(Issue Queue, IQ):一个20条目的整数发射队列和一个15条目的浮点发射队列。这种分离设计反映了整数和浮点指令流的不同特征——整数指令通常更多、延迟更短、依赖更密集,而浮点指令数量较少但延迟更长。

整数发射队列的结构。20条目的整数发射队列为4个整数功能单元(分布在2个Cluster中)提供指令。每个条目记录以下信息:

  • 操作码:指令的操作类型。

  • 源操作数标识:两个源操作数对应的物理寄存器编号。

  • 源就绪位:两个1-bit标志,指示每个源操作数是否已经就绪(值已经可用于旁路或已经写入寄存器文件)。

  • 目标物理寄存器编号:该指令的结果将写入的物理寄存器。

  • 年龄信息:用于仲裁的年龄标记。

发射策略。21264的整数发射队列采用最老优先(oldest-first)的发射策略,配合投机唤醒(speculative wakeup)机制。当一条指令的所有源操作数都就绪时,该指令成为可发射(ready to issue)状态。每周期,发射逻辑从所有可发射的指令中选择最多4条(受功能单元可用性约束)进行发射。

投机唤醒是21264发射队列的一个重要特性,将在存储器访问部分详细讨论——简单来说,当一条Load指令被发射时,发射队列会乐观地假设该Load将命中D-Cache,在Load实际完成之前就唤醒依赖于该Load结果的后续指令。如果Load确实命中Cache,后续指令可以在Load完成后的下一个周期开始执行,实现最短的Load-use延迟。如果Load未命中Cache,已经被唤醒的后续指令需要被取消和重发。

浮点发射队列。15条目的浮点发射队列结构与整数发射队列类似,但服务于2个浮点功能单元(FADD和FMUL)。浮点指令的执行延迟较长(4–6个周期),因此浮点发射队列中的指令平均等待时间更长,15个条目在大多数浮点密集的代码中足够使用。

浮点发射队列容量的考量。为什么浮点发射队列只有15个条目而非20个?这主要基于以下考虑:

  1. 浮点指令密度低:在典型的混合工作负载中,浮点指令占比不到20%。4-wide取指每周期最多取4条指令,其中浮点指令平均不到1条。15个条目可以缓冲约15个周期的浮点指令流,对于大多数代码来说绑绑有余。

  2. 浮点延迟可预测:浮点运算的延迟是固定的(加法4周期、乘法4周期),不存在像Load那样的可变延迟(Cache命中vs缺失)。这意味着浮点发射队列中的指令的唤醒时间是确定的,队列不需要额外容量来应对不确定性。

  3. 浮点发射宽度窄:浮点单元只有2个(FADD + FMUL),每周期最多发射2条浮点指令。15个条目提供了15/2=7.515/2 = 7.5个周期的缓冲深度,足够覆盖浮点流水线的深度。

唯一可能使浮点发射队列成为瓶颈的场景是浮点除法密集的代码——由于除法非流水线化、占用FMUL单元15–30个周期,期间所有等待FMUL的指令都堆积在发射队列中。如果除法频率较高,15个条目可能不足以缓冲等待的指令。但在实际应用中,浮点除法的频率通常很低(占浮点指令的不到5%),这一场景很少发生。

设计提示

21264的发射队列容量(整数20条目、浮点15条目)在今天看来非常小——现代处理器的发射队列通常有数百个条目。但在1998年的工艺条件下,发射队列的每一个条目都包含复杂的内容寻址(Content-Addressable Memory, CAM)逻辑用于唤醒,以及优先级仲裁逻辑用于选择。这些逻辑的面积和延迟随条目数量超线性增长(唤醒逻辑为O(n×p)O(n \times p),其中nn为条目数、pp为端口数;仲裁逻辑为O(nlogn)O(n \log n)),因此20和15条目实际上已经接近当时工艺条件下的实际极限。21264通过其他机制(如大ROB窗口和高精度分支预测)来弥补发射队列容量的不足。

发射队列的压缩与管理。21264的发射队列采用压缩式(compacting)设计:当一条指令被发射后,其在队列中的条目被释放,后续条目向前移动以填充空位,使得新指令总是被插入队列的尾部。这种压缩设计保持了指令在队列中的程序顺序,使得"最老优先"的仲裁策略可以简单地通过选择队列头部的就绪指令来实现。

压缩式设计的优势是仲裁逻辑简单,但缺点是每周期需要移动多个条目,带来较高的功耗和布线复杂度。后来的处理器设计(如Intel P6/Core系列)普遍采用非压缩式(non-compacting)发射队列,使用年龄矩阵(age matrix)进行仲裁,避免了条目移动的开销。

唤醒与选择逻辑

发射队列的核心逻辑由两部分组成:唤醒逻辑(Wakeup Logic)和选择逻辑(Select Logic)。这两部分在每个时钟周期内串行工作,构成发射阶段的关键路径。

唤醒逻辑。当一条指令被发射并即将产生结果时,它的目标物理寄存器编号被广播到发射队列中所有条目的源操作数比较器。每个条目有两个源操作数比较器(对应两个源),每个比较器将广播的物理寄存器编号与本条目等待的源物理寄存器编号进行比较。如果匹配,该源的就绪位被设置为1。

对于21264的整数发射队列,唤醒逻辑的规模为:

  • 广播端口数:4(每周期最多4条整数指令被发射,各广播1个结果标识符)。

  • 条目数:20。

  • 每条目比较器数:2(两个源操作数)。

  • 总比较器数:4×20×2=1604 \times 20 \times 2 = 160个7-bit比较器。

这160个比较器构成一个4×404 \times 40的CAM(内容寻址存储器)阵列。每个比较器由7个XOR门和一个7输入AND门组成,面积和功耗不可忽视。

选择逻辑。在唤醒逻辑确定了哪些条目处于就绪状态后,选择逻辑从所有就绪条目中挑选最多4条(受功能单元可用性约束)进行发射。21264采用最老优先策略,对于压缩式发射队列,这等价于选择队列中位置最靠前的就绪条目。

选择逻辑需要在单周期内完成以下工作:

  1. 扫描20个条目的就绪状态。

  2. 选择最多4个就绪条目(每个Cluster最多2个)。

  3. 考虑功能单元的类型约束——某些操作(如整数乘法)只能在特定功能单元上执行。

  4. 输出所选指令的控制信号,驱动寄存器文件读取和功能单元输入。

唤醒与选择的时序关系。唤醒和选择在每个时钟周期内必须串行完成:先完成唤醒(设置就绪位),然后选择逻辑基于更新后的就绪位进行选择。这个串行路径(唤醒延迟+选择延迟)是发射阶段的关键时序路径。在21264的目标频率下,这一关键路径决定了发射队列的最大规模——更多的条目意味着更多的比较器和更宽的选择仲裁,直接增加关键路径延迟。

硬件描述 6 — 唤醒-选择-执行循环

在理想情况下,当依赖链上的指令back-to-back执行时,处理器需要在每个周期内完成以下循环:

  1. 指令A在Stage 5执行完毕,其结果可用于旁路。

  2. 同一周期,A的结果标识符被广播到发射队列(唤醒逻辑),唤醒依赖A的指令B。

  3. 下一个周期(Stage 5),B被选择并发射,通过旁路获得A的结果,开始执行。

这意味着唤醒广播和选择仲裁必须在一个周期内完成——如果这个循环需要两个周期,则依赖链上每条指令之间的最小间隔变为2个周期,有效IPC将大幅降低。21264通过限制发射队列规模(20条目)和采用压缩式设计来确保这一关键循环在单周期内完成。

这一限制解释了为什么21264的发射队列如此小——不是因为设计者不想要更大的队列,而是时序约束不允许在目标频率下实现更大的单周期唤醒-选择循环。

指令的解码与Slot分配

21264的解码阶段(Stage 2, Slot/Decode)负责对从I-Cache读出的4条指令进行全解码,并分配Slot。Slot分配决定了每条指令将使用哪种类型的功能单元。

Alpha指令的解码。由于Alpha ISA的固定32-bit指令格式,全解码相对简单。解码器需要从指令字中提取:操作码(6-bit主操作码 + 功能码)、源寄存器编号(2个5-bit字段)、目标寄存器编号(1个5-bit字段)、立即数(如果有)、以及分支偏移量(如果是分支指令)。

21264有4个并行的解码器,每个处理一条指令。解码器的输出包括:

  • 内部操作类型(用于发射队列和功能单元控制)。

  • 源/目标逻辑寄存器编号(传递给重命名阶段)。

  • 指令分类:整数运算、存储器访问、浮点运算、分支。

  • 分支方向预测结果(来自Tournament预测器,在此阶段可用)。

Slot分配策略。21264将解码后的指令分配到不同的Slot,每个Slot对应一种执行资源:

  • 整数Slot 0/1:分配给整数运算和存储器地址计算指令。每周期最多4条整数指令进入整数发射队列。

  • 浮点Slot 0/1:分配给浮点运算指令。每周期最多2条浮点指令进入浮点发射队列。

如果一个周期内的4条指令中浮点指令超过2条,多余的浮点指令需要被延迟到下一个周期处理,导致前端暂停。在实际程序中,由于整数指令通常占多数,这种暂停是罕见的。

ROB条目分配

在重命名阶段,每条指令被分配一个ROB条目。21264的ROB采用循环缓冲区组织,分配过程是顺序的——新指令被插入到ROB尾部。

ROB分配与物理寄存器分配的关系。ROB分配和物理寄存器分配在同一个流水级(Stage 3, Map/Rename)完成,但它们是独立的资源。一条指令总是需要一个ROB条目(用于维护程序顺序),但不一定需要新的物理寄存器(如果指令没有目标寄存器,如Store指令或分支指令)。

ROB满暂停。当ROB的所有80个条目都被占用(ROB满)时,前端必须暂停——无法再分配新的ROB条目给进入重命名阶段的指令。ROB满暂停是21264中最常见的性能瓶颈之一,尤其在以下场景中:

  • 长延迟L2/主存缺失:ROB头部的指令等待主存数据(100+周期),期间后续指令不断填入ROB但无法退休。如果等待时间超过80/4=2080/4 = 20个周期(80个ROB条目 / 4-wide取指),ROB将满。

  • 长延迟浮点除法:浮点除法的15–30周期延迟同样会导致ROB积压。

  • 串行化指令:某些Alpha特权指令(如TRAPB、存储屏障指令)要求在执行前排空流水线,导致ROB暂时无法释放。

ROB容量与性能的关系。21264的80条目ROB在当时是最大的——同时代的MIPS R10000只有32条目,Intel P6约40条目。更大的ROB允许处理器在长延迟事件(如Cache缺失)期间维持更多的in-flight指令,有效提高存储器级并行度。

可以用Little定律(Little’s Law)来理解ROB容量的重要性:在稳态下,ROB中的平均指令数等于指令的平均吞吐量乘以平均驻留时间。如果目标IPC为2.0、平均指令从进入到退休的延迟为20个周期,则ROB需要至少2.0×20=402.0 \times 20 = 40个条目。80条目的ROB提供了2倍的余量,可以应对延迟波动和突发的长延迟事件。

性能分析 3 — ROB容量对存储器级并行的影响

ROB容量直接影响处理器的存储器级并行(MLP)——同时维持的未完成Cache缺失数量。以下分析说明这一关系:

假设程序中每100条指令有2条L2缺失(缺失率2%),每次L2访问延迟15个周期。在80条目的ROB下:

  • ROB可以容纳80条指令,其中约80×0.02=1.680 \times 0.02 = 1.6条是缺失Load。

  • 这意味着平均约1.6个缺失可以同时在飞——略少于MAF的8条目容量,说明ROB是限制MLP的瓶颈而非MAF。

  • 如果ROB容量为160条目,平均可以有160×0.02=3.2160 \times 0.02 = 3.2个同时在飞的缺失,MLP几乎翻倍。

这一分析解释了为什么后续的处理器设计不断增大ROB容量——Intel Haswell有192条目,Skylake有224条目,Apple M1有约600条目——更大的ROB允许在长延迟缺失期间维持更多的in-flight指令和更高的MLP。

从21264的80条目到Apple M1的约600条目,ROB容量在25年间增长了约7.5倍。这个增长率远慢于同期晶体管密度的约4000倍增长,说明ROB容量的扩展不仅受面积约束,更受关键路径延迟(更大的ROB需要更宽的仲裁逻辑)和功耗约束(每个ROB条目每周期参与状态检查和更新)。21264在1998年以80条目实现了当时最大的指令窗口,这一选择在有限晶体管预算下是极具远见的。

执行单元

21264的执行单元是其微架构中另一个标志性设计——整数执行单元采用了双Cluster(dual-cluster)结构,这是商用处理器中最早的Cluster化设计之一,对后续的处理器设计产生了深远影响。

整数执行单元的Cluster结构

回顾第 35.0 章中关于Cluster结构的系统讨论:Cluster化的本质是用少量跨Cluster通信延迟换取旁路网络和寄存器文件的物理可行性。21264正是这一技术的工业先驱——它的双Cluster设计在1998年就解决了第 35.0 章中讨论的O(N2)O(N^2)旁路复杂度问题。同时,21264的Store Wait Map是第 36.0 章中Store Set投机消歧技术的直系前身——两者共享相同的设计理念(用PC索引的历史表来预测Load-Store冲突),只是后者在数据结构上更加精细。

21264拥有4个整数功能单元,被分成两个Cluster:Cluster 0和Cluster 1,每个Cluster包含2个功能单元。每个Cluster有自己独立的物理寄存器文件副本(即两个Cluster各自持有一份完整的80个整数物理寄存器的副本),以及独立的旁路网络(bypass network)。

Cluster化的动机。为什么不使用一个集中式的4端口物理寄存器文件?答案在于物理寄存器文件的端口数量扩展问题。一个4-wide发射的处理器需要物理寄存器文件提供大量的读写端口:

  • 读端口:每条指令最多2个源操作数,4条指令共需8个读端口。

  • 写端口:每条指令1个结果,4条指令共需4个写端口。

一个80×\times64-bit的SRAM阵列提供12个端口(8读+4写)的面积约正比于(8+4)2=144(8+4)^2 = 144——端口数的平方。这在0.35 μm工艺下会导致物理寄存器文件面积过大、访问延迟过长,无法满足目标频率。

Cluster化的解决方案是将这个12端口的集中式寄存器文件拆分为两个6端口的寄存器文件(每个Cluster 4读+2写),面积各自约正比于(4+2)2=36(4+2)^2 = 36,总面积2×36=722 \times 36 = 72,仅为集中式方案的一半。更重要的是,每个寄存器文件的访问延迟也因端口减少而显著缩短,使得在目标频率下可以在单周期内完成寄存器文件读取。

Alpha 21264的双Cluster整数执行结构。每个Cluster有独立的物理寄存器文件副本和旁路网络。Cluster间通过跨Cluster总线通信,引入额外1周期延迟。
Alpha 21264的双Cluster整数执行结构。每个Cluster有独立的物理寄存器文件副本和旁路网络。Cluster间通过跨Cluster总线通信,引入额外1周期延迟。

Cluster间通信。两个Cluster之间通过一条跨Cluster总线(cross-cluster bus)连接。当一条指令在Cluster 0中执行,其结果需要被Cluster 1中的另一条指令使用时,结果值通过跨Cluster总线从Cluster 0的寄存器文件副本传输到Cluster 1的寄存器文件副本。这一传输需要额外的1个周期延迟。

这意味着:

  • 同Cluster的数据转发:通过本地旁路网络,延迟为0个额外周期。即如果指令A在Cluster 0的ALU 0中执行,指令B也在Cluster 0中执行且依赖A的结果,则B可以在A执行完成后的下一个周期立即使用A的结果(通过旁路)。

  • 跨Cluster的数据转发:延迟为1个额外周期。即如果指令A在Cluster 0中执行,指令B在Cluster 1中执行且依赖A的结果,则B必须在A执行完成后等待2个周期才能使用结果(1个周期的跨Cluster传输 + 正常的旁路延迟)。

指令的Cluster分配。21264在发射阶段决定将每条整数指令分配到哪个Cluster。分配策略考虑以下因素:

  1. 依赖关系:如果一条指令的源操作数的产生者已经在某个Cluster中,则该指令应尽量被分配到同一个Cluster,以避免跨Cluster延迟。

  2. 负载均衡:两个Cluster的工作负载应尽可能均衡,避免一个Cluster过载而另一个空闲。

  3. 功能单元可用性:某些操作(如整数乘法)可能只在特定功能单元上执行,限制了分配选择。

21264的Cluster分配器使用一种相对简单的启发式策略:优先将指令分配到其源操作数所在的Cluster(减少跨Cluster延迟),在两个Cluster都可行时选择负载较轻的一个。

整数功能单元的能力

21264的4个整数功能单元并非完全对称——不同的功能单元支持不同的操作子集。这种非对称设计是面积优化的结果:不是每个功能单元都需要实现所有操作,将昂贵的操作集中在少数功能单元上可以节省芯片面积。

功能单元的分工。21264的4个整数ALU的能力分配如下:

  • ALU 0(Cluster 0):支持所有基本算术运算(加减法、逻辑运算、移位)、条件移动(CMOV)、以及整数乘法。整数乘法是一个多周期操作(延迟约7个周期),使用迭代乘法器实现,但乘法器是流水线化的(吞吐量为1条乘法/周期)。

  • ALU 1(Cluster 0):支持所有基本算术运算和条件移动。不支持整数乘法。

  • ALU 2(Cluster 1):支持所有基本算术运算、条件移动。

  • ALU 3(Cluster 1):支持所有基本算术运算、条件移动、以及分支解析。分支指令的方向解析(条件判断)由ALU 3完成。

地址计算单元。每个Cluster还包含一个专用的地址计算单元(Address Generator),用于计算Load/Store指令的有效地址。地址计算本质上是一个整数加法操作(base + displacement),因此可以由普通ALU执行。但21264将地址计算分离到专用单元中,有以下好处:

  • 地址计算不占用通用ALU的执行带宽,使得存储器指令和算术指令可以并行执行。

  • 地址计算单元的输出直接连接到D-Cache的地址输入端和D-TLB,省去了通过旁路网络的额外延迟。

  • 简化了发射逻辑——存储器指令和算术指令使用不同的功能单元,不存在资源冲突。

非对称功能单元对调度的影响。由于整数乘法只在ALU 0上执行,所有整数乘法指令都必须被分配到Cluster 0。类似地,分支指令的解析只在ALU 3上执行,都被分配到Cluster 1。这种约束限制了Cluster分配的灵活性——在乘法和分支密集的代码段中,某个Cluster可能过载。发射逻辑需要在遵循功能单元约束的前提下尽量保持负载均衡。

硬件描述 7 — 功能单元延迟汇总

操作类型延迟(周期)可执行的功能单元
加法/减法/逻辑1ALU 0/1/2/3
移位1ALU 0/1/2/3
条件移动(CMOV)1ALU 0/1/2/3
整数乘法7(流水线化)ALU 0
分支解析1ALU 3
地址计算1地址计算 0/1

性能分析 4 — Cluster间延迟的性能影响

跨Cluster的1周期额外延迟对性能的影响取决于程序中跨Cluster数据依赖的频率。根据21264设计团队的分析,在SPECint95基准测试中,大约20%–30%的整数指令对存在跨Cluster依赖,这意味着这些指令对的有效延迟从1周期增加到2周期。

为了量化这一影响,可以进行一个简化的分析。假设一个整数密集的程序中,平均每条指令的关键路径延迟为:

Leff=Lbase+fcross×LcrossL_{\text{eff}} = L_{\text{base}} + f_{\text{cross}} \times L_{\text{cross}}

其中Lbase=1L_{\text{base}} = 1周期为基础ALU延迟,fcross0.25f_{\text{cross}} \approx 0.25为跨Cluster依赖的频率,Lcross=1L_{\text{cross}} = 1周期为跨Cluster额外延迟。则有效平均延迟为:

Leff=1+0.25×1=1.25周期L_{\text{eff}} = 1 + 0.25 \times 1 = 1.25\text{周期}

这约相当于IPC损失约5%–10%。然而,如果不采用Cluster化设计,集中式寄存器文件的访问延迟可能增加到2个周期(在目标频率下),这将导致所有指令对的有效延迟变为2周期——远比Cluster化设计的5%–10%损失严重。这正是Cluster化设计的精妙之处:用少量指令对的额外延迟换取大多数指令对的低延迟。

寄存器文件一致性。两个Cluster的寄存器文件必须保持一致——当一条指令在Cluster 0中执行并写入结果时,该结果不仅写入Cluster 0的本地寄存器文件,还需要在下一个周期通过跨Cluster总线写入Cluster 1的寄存器文件。这种写广播(write broadcast)保证了两个副本的内容始终一致(最多相差1个周期的延迟)。

这种设计的一个重要约束是跨Cluster总线的带宽:每周期最多从每个Cluster向另一个Cluster传输2个64-bit的值(等于每个Cluster的ALU数量)。由于两个方向的传输互不干扰(使用独立的总线),总线带宽通常不会成为瓶颈。

物理寄存器文件副本的一致性维护

双Cluster的物理寄存器文件副本一致性是21264 Cluster化设计中最关键的工程问题之一。两个Cluster各持有80个64位整数物理寄存器的完整副本,必须保证在任何时刻,两个副本中同一物理寄存器的值是一致的(或至少在使用前一致)。

写广播协议。21264采用的一致性维护协议基于写广播(write broadcast):

  1. 当Cluster 0中的ALU产生一个结果时,该结果在同一周期写入Cluster 0的本地物理寄存器文件。

  2. 下一个周期,该结果通过跨Cluster总线传输到Cluster 1,写入Cluster 1的物理寄存器文件。

  3. 对称地,Cluster 1中产生的结果也在下一个周期传输到Cluster 0。

这意味着两个副本之间存在最多1个周期的不一致窗口。在这个窗口内,Cluster 0可能已经持有某个物理寄存器的最新值,而Cluster 1中对应的副本仍然是旧值。

不一致窗口的处理。这个1周期的不一致窗口不会导致正确性问题,原因在于:如果一条指令B依赖于Cluster 0中刚执行完的指令A的结果,且B被调度到Cluster 1执行,那么B在发射时需要等待A的结果通过跨Cluster总线到达Cluster 1。发射队列的唤醒逻辑会将跨Cluster的唤醒延迟额外延长1个周期——即A的结果广播唤醒同Cluster的依赖者时,跨Cluster的依赖者会在下一个周期才被唤醒。这保证了B在Cluster 1中读取物理寄存器文件时,A的结果已经到达。

写端口分配。每个Cluster的物理寄存器文件需要提供以下写端口:

  • 2个本地写端口:用于本Cluster中2个ALU的执行结果写回。

  • 2个远程写端口:用于接收来自对方Cluster的2个执行结果。

  • 额外的写端口:用于Load数据写回(Load结果需要写入两个Cluster的寄存器文件)。

因此每个Cluster的物理寄存器文件实际上需要至少5个写端口(2本地ALU + 2远程ALU + 1 Load),加上4个读端口(本地2条指令×\times2个源),总端口数为9个。这比集中式方案的12个端口少了约25%,加上每个端口的负载更小(只驱动半数功能单元),物理寄存器文件的面积和时序都得到了显著改善。

硬件描述 8 — PRF副本一致性的时序图

以下示例说明PRF副本一致性的时序关系。假设指令A在Cluster 0执行,产生结果写入物理寄存器P17;指令B在Cluster 1执行,需要读取P17作为源操作数:

周期事件Cluster 0的P17Cluster 1的P17
TA在Cluster 0执行完毕新值(旁路可用)旧值
T+1A的结果通过总线传输新值(已写入PRF)新值写入PRF
T+1B在Cluster 1被唤醒
T+2B在Cluster 1发射并读PRF新值(可读)

可以看到,从A完成(周期T)到B可以在Cluster 1中使用结果(周期T+2),总延迟为2个周期——比同Cluster的1周期延迟多了1个周期。

Cluster分配策略的详细分析

21264的Cluster分配策略对性能有显著影响——不良的分配可能导致大量跨Cluster通信,增加有效延迟并降低IPC。21264在Map/重命名阶段(Stage 3)决定每条整数指令的Cluster分配。

分配算法。21264使用的分配启发式可以描述为以下优先级规则:

  1. 规则1——依赖性跟随(Dependence Following):如果指令的源操作数的生产者指令已经被分配到某个Cluster,则该指令优先分配到同一个Cluster。这最大限度地利用本地旁路网络,避免跨Cluster延迟。

  2. 规则2——负载均衡(Load Balancing):如果规则1无法确定唯一的Cluster(例如两个源来自不同Cluster,或源操作数已经在两个Cluster中都就绪),则分配到当前负载较轻的Cluster。负载通过简单的计数器跟踪每个Cluster中未完成指令的数量。

  3. 规则3——默认分配:如果以上规则都无法区分(两个Cluster负载相同、源操作数均就绪),则采用轮转(round-robin)分配。

依赖性跟随的复杂性。当一条指令有两个源操作数,且它们的生产者分别在不同Cluster中时,依赖性跟随规则面临冲突。在这种情况下,21264倾向于跟随关键路径上的源——即延迟更长的那个源。具体地,如果源A的生产者尚未执行(源不就绪),而源B的生产者已经执行完毕(源已就绪),则跟随源A的Cluster,因为源A是决定该指令何时可以执行的关键因素。

存储器指令的Cluster分配。Load和Store指令的地址计算由整数ALU完成。21264在每个Cluster中各有一个地址计算单元(地址计算0和地址计算1),因此存储器指令也需要进行Cluster分配。存储器指令的分配同样遵循依赖性跟随规则——基地址寄存器值的生产者在哪个Cluster,存储器指令就分配到哪个Cluster。

性能分析 5 — Cluster分配策略对IPC的影响

Cluster分配策略的质量可以通过跨Cluster通信的频率来衡量。理想的分配策略应该最小化跨Cluster依赖的数量。根据21264设计团队对SPECint95基准测试的分析:

  • 最优分配(离线算法,已知完整依赖图):跨Cluster依赖约占12%–15%。

  • 21264的在线启发式:跨Cluster依赖约占20%–30%。

  • 简单轮转分配(不考虑依赖性):跨Cluster依赖约占45%–50%。

21264的启发式分配策略比简单轮转好了约20个百分点,但与最优方案仍有约10–15个百分点的差距。这个差距源于在线算法在分配时无法看到未来的指令依赖关系。尽管如此,21264的策略在实现复杂度和性能之间取得了良好的平衡。

旁路网络的组织

旁路网络(Bypass Network,也称Forwarding Network)是执行单元中的关键数据路径。它允许一条刚执行完毕的指令的结果直接传递给下一条依赖指令,而不需要等待结果写入物理寄存器文件后再读出。在21264的Cluster化设计中,旁路网络的组织与Cluster结构紧密耦合。

Cluster内的旁路。每个Cluster有2个整数ALU和1个地址计算单元。Cluster内的旁路网络需要支持以下转发路径:

  • ALU 0的结果 \to ALU 0的输入(自身旁路,支持back-to-back依赖链)。

  • ALU 0的结果 \to ALU 1的输入(同Cluster跨功能单元旁路)。

  • ALU 1的结果 \to ALU 0的输入。

  • ALU 1的结果 \to ALU 1的输入。

  • 两个ALU的结果 \to 地址计算单元的输入。

每个旁路路径是一个64-bit宽的多路选择器,选择来自不同源(本地ALU结果、远程Cluster结果、寄存器文件读出值)的数据。对于每个Cluster内的每个功能单元输入端,旁路MUX的输入包括:2个本地ALU结果、2个远程ALU结果(经跨Cluster总线)、1个Load结果、以及寄存器文件读出值——共6个选择源。

旁路MUX的时序影响。6-input的64-bit MUX在0.35 μm工艺下的延迟约为0.3 ns。这一延迟直接加在执行阶段的关键路径上:功能单元的输入需要先通过旁路MUX选择正确的源数据,然后才能开始ALU运算。在2 ns的时钟周期中,0.3 ns的MUX延迟占了15%的时间预算。

如果不采用Cluster化设计,4个ALU的旁路网络需要每个输入端有一个更宽的MUX(至少8个输入源——4个ALU结果 + 1个Load结果 + 寄存器文件读出值等),延迟更长。Cluster化设计将这个MUX的宽度从8+缩减到6,是旁路网络时序优化的重要手段。

旁路网络的布线挑战。旁路网络不仅是逻辑设计的挑战,更是物理布局的挑战。每条旁路路径是一条64-bit宽的全芯片级信号线——它需要从功能单元的输出端横跨到另一个功能单元的输入端。在0.35 μm工艺下,长距离的信号线有显著的RC延迟。

21264的Cluster化设计在物理布局上的优势在于:Cluster内的旁路路径很短(两个ALU和寄存器文件物理上紧密相邻),而跨Cluster的旁路路径虽然较长,但被显式地分配了额外1个周期的时间预算,不需要在单周期内完成传输。这种"近距离快速旁路+远距离慢速旁路"的分层方案,使得旁路网络的物理实现在目标频率下可行。

案例研究 3 — 旁路网络规模的增长趋势

旁路网络的规模(MUX宽度和路径数量)随发射宽度的增加而快速增长。对于WW-wide发射的处理器,如果使用集中式寄存器文件,旁路MUX的宽度约为W+1W + 1WW个ALU结果 + 寄存器文件读出值),总路径数约为W×2×(W+1)W \times 2 \times (W + 1)WW个功能单元 ×\times 2个源操作数 ×\times (W+1)(W+1)个选择源)。

  • 21264(4-wide, 集中式):MUX宽度\sim5,总路径\sim40。

  • 21264(4-wide, 2-Cluster):每个Cluster的MUX宽度\sim6(2本地+2远程+Load+PRF),每个Cluster总路径\sim24。两个Cluster共48。数量略多于集中式,但每条路径更短。

  • 假设8-wide(集中式):MUX宽度\sim9,总路径\sim144。这已经是布线噩梦。

  • 假设8-wide(4-Cluster,如EV8计划):每个Cluster MUX宽度\sim8,每个Cluster总路径\sim32。四个Cluster共128。虽然路径总数相近,但每条路径在局部完成,物理上可行。

这一分析说明了为什么Cluster化设计在宽发射处理器中几乎是必须的——不是因为设计者想要Cluster间延迟,而是因为集中式旁路网络的物理实现在宽发射时变得不可行。

案例研究 4 — Cluster化设计的后续发展

21264的双Cluster设计开创了处理器Cluster化执行的先河。此后,Cluster化设计在多款处理器中得到应用:

  • Alpha 21464(EV8):21264的后继设计(最终未量产)计划采用更复杂的4-Cluster结构,每个Cluster包含2个ALU,支持8-wide发射。

  • Intel Pentium 4(2000年):虽然不完全是Cluster化设计,但其20+级的深度流水线中采用了"快速ALU"和"慢速ALU"的分离,概念上与Cluster化类似。

  • AMD Bulldozer(2011年):采用了一种极端的Cluster化——CMT(Cluster Multi-Threading),两个整数Cluster共享一个浮点单元和前端。

  • 现代ARM核心:ARM Cortex-A77及后续核心在整数执行中采用了类似Cluster化的组织方式,将多个ALU分成子组以降低旁路网络的复杂度。

Cluster化设计的核心权衡——降低寄存器文件端口需求和旁路网络复杂度,代价是引入跨Cluster通信延迟——在所有这些设计中都是相同的。随着工艺进步和导线延迟相对增大,Cluster化设计在现代处理器中变得越来越普遍。

浮点执行单元

21264的浮点执行单元(Fbox)相对于整数单元来说结构更为传统。Fbox包含两个浮点功能单元:

  • FADD:浮点加法/减法单元,同时处理浮点比较、浮点转整数等操作。延迟为4个周期。

  • FMUL:浮点乘法单元,同时处理浮点除法和浮点平方根。乘法延迟为4个周期;除法和平方根为非流水线化操作,延迟为数十个周期。

浮点单元不采用Cluster化设计——两个浮点功能单元共享同一个72×\times64-bit的浮点物理寄存器文件。这一选择有几个原因:

  1. 浮点指令较少:在大多数程序中,浮点指令的密度远低于整数指令,因此浮点发射宽度只需2-wide(而非整数的4-wide),寄存器文件端口需求较低。

  2. 浮点延迟较长:浮点操作的延迟为4个周期,这意味着旁路网络有更多时间传输数据,跨模块延迟的相对影响较小。

  3. 浮点旁路较简单:只有2个功能单元,旁路网络只需4个读端口+2个写端口,在单一寄存器文件中即可实现。

浮点流水线。浮点指令在重命名阶段被写入15条目的浮点发射队列。从发射队列发射后,经过4个执行流水级:

  1. FP Stage 1:读取浮点物理寄存器文件中的源操作数。

  2. FP Stage 2–3:浮点运算的核心执行阶段(对于4周期延迟的操作)。

  3. FP Stage 4:结果写回浮点物理寄存器文件。

浮点加法和乘法都是完全流水线化的(fully pipelined),即每周期可以接受一条新指令,吞吐量为1条指令/周期/功能单元。浮点除法和平方根不是流水线化的,在执行期间会阻塞FMUL单元。

浮点运算的精度控制。Alpha ISA支持IEEE 754标准的单精度(32位S_Floating)和双精度(64位T_Floating)浮点格式,以及VAX特有的F_Floating和G_Floating格式(用于与DEC传统软件的兼容性)。21264的浮点单元可以在不同精度模式下运行,精度选择由指令操作码中的功能码字段指定。

浮点除法与平方根。FMUL单元兼任浮点除法和平方根运算。除法和平方根使用迭代算法(SRT除法或Newton-Raphson迭代),不是流水线化的——在除法或平方根操作执行期间,FMUL单元被占用,无法接受新的乘法指令。除法的延迟约为15–20个周期(取决于精度),平方根约为20–30个周期。在浮点除法密集的代码中,FMUL单元的独占会导致浮点吞吐量显著下降。

整数-浮点数据传输。Alpha ISA提供了专门的整数-浮点数据传输指令(ITOFFTOI),用于在整数寄存器文件和浮点寄存器文件之间搬移数据。在21264中,这些传输指令需要经过存储器流水线——数据首先从源寄存器文件写入一个中间缓冲区,然后在下一个周期被读入目标寄存器文件。这一间接路径引入了额外的延迟(约3–4个周期),但避免了在整数和浮点寄存器文件之间建立直接连接的布线复杂度。

设计提示

21264的整数-浮点传输通过存储器路径进行的设计选择,在现代处理器中仍然常见。AMD Zen系列处理器的整数-浮点数据传输同样需要经过Load/Store单元,延迟约5–7个周期。这反映了一个普遍的设计权衡:整数和浮点寄存器文件在物理上通常位于芯片的不同区域(整数寄存器文件靠近整数ALU,浮点寄存器文件靠近浮点单元),建立它们之间的直接高速连接会增加布线拥堵和跨芯片的信号延迟。通过存储器路径中转,虽然延迟增加,但布局布线更加合理。对于大多数程序来说,整数-浮点传输指令的频率很低(通常不到1%),额外延迟的性能影响可以忽略。

存储器的访问

21264的存储器子系统(Mbox)负责处理Load和Store指令的执行。21264在存储器访问方面引入了两个重要的投机技术:Speculative Disambiguation(投机存储器消歧)和Load Hit/Miss Prediction(Load命中/缺失预测)。这两个技术共同作用,使得21264的存储器流水线在大多数情况下能够以最低延迟处理Load指令。

存储器流水线概述。21264的D-Cache为64 KB、2-way set-associative,访问延迟为3个周期。21264每周期可以发射2条存储器访问指令(2个地址计算单元分别位于两个整数Cluster中)。存储器流水线的主要阶段包括:

  1. 地址计算:使用整数ALU计算有效地址(base + offset)。

  2. D-Cache Tag查找和数据读取:使用有效地址访问D-Cache,读出数据和Tag。

  3. Tag比较和数据选择:比较Tag确定命中,选择正确Way的数据。

对于Store指令,计算出的地址和数据被写入Store Buffer,在指令提交时才将数据写入D-Cache。Store Buffer允许后续的Load指令通过Store-to-Load转发(store forwarding)直接从Store Buffer中获取数据,而不需要等待数据写入Cache。

Store Buffer与Store-to-Load转发

21264的Store Buffer是存储器流水线中的关键结构。它在Store指令执行(地址和数据就绪)与Store指令提交(数据写入D-Cache)之间提供缓冲,同时支持Store-to-Load转发以降低Load延迟。

Store Buffer的结构。21264的Store Buffer包含32个条目。每个条目记录以下信息:

  • Store地址(64-bit虚拟地址):Store指令的目标内存地址。

  • Store数据(64-bit数据值):要写入内存的数据。

  • 数据大小:1字节、2字节、4字节或8字节。

  • 地址有效位:标识Store的地址是否已经计算完成。对于地址尚未就绪的Store,该位为0。

  • 数据有效位:标识Store的数据是否已经就绪。

  • ROB条目指针:指向该Store在ROB中对应条目的位置,用于确定程序顺序。

Store Buffer中的条目按程序顺序排列(使用循环缓冲区组织),这使得后续Load指令可以按顺序搜索Store Buffer,查找可能的地址匹配。

Store-to-Load转发机制。当一条Load指令在存储器流水线中计算出地址后,它需要同时查找D-Cache和Store Buffer。Store Buffer的查找逻辑将Load地址与所有在程序顺序上位于该Load之前的Store条目进行地址比较。如果找到匹配:

  1. 如果匹配的Store的数据有效位为1(数据已就绪),且Store的大小覆盖了Load的整个访问范围,则直接从Store Buffer转发数据,D-Cache的结果被忽略。这就是Store-to-Load转发。

  2. 如果匹配但数据尚未就绪(数据有效位为0),则Load必须等待直到Store数据就绪后再转发。

  3. 如果有多个匹配的Store(同一地址被多次写入),则使用在程序顺序上最近的那个Store的数据(最年轻的匹配Store)。

转发的时序。Store-to-Load转发的延迟与D-Cache命中的延迟相同(3个周期),因为Store Buffer查找与D-Cache查找并行进行。转发成功时,从Load的角度看,数据获取延迟与D-Cache命中完全相同,不会引入额外的流水线气泡。

部分转发的限制。21264的Store-to-Load转发机制不支持部分转发——即当一条Load跨越两个Store的写入范围时(例如Load读取8字节,但这8字节的前4字节由一个Store写入,后4字节由另一个Store写入),转发不能进行,Load必须等待两个Store都提交并写入D-Cache后,再从D-Cache读取合并后的数据。这种部分重叠的情况在实际程序中很少发生(通常编译器生成的Store和Load使用一致的数据大小和对齐方式),但在某些低级内存操作(如memcpy的非对齐实现)中可能出现。

Store Buffer满暂停。当Store Buffer的32个条目全部被占用时,新的Store指令无法进入Store Buffer,发射逻辑必须暂停对Store指令的发射。这种暂停的发生概率取决于Store指令的频率和提交(D-Cache写入)的速度。在典型的整数程序中,Store指令约占总指令的10%–15%,平均每周期不到1条Store被提交(退休宽度为11,但Store写入D-Cache的带宽有限)。32个条目通常足以覆盖Store从发射到提交的延迟。

但在某些Store密集的代码中(如大规模memset或数组初始化),Store Buffer可能成为瓶颈。这类代码中Store频率极高(几乎每条指令都是Store),而D-Cache的写入带宽有限(每周期只能提交1–2个Store的写入),Store Buffer会迅速填满。

硬件描述 9 — Store Buffer的地址匹配逻辑

Store Buffer的地址匹配使用CAM结构。当一条Load指令计算出有效地址后,该地址被送入Store Buffer的CAM阵列,与所有有效条目的Store地址并行比较。比较需要考虑以下因素:

  • 地址对齐和大小:比较不仅检查地址是否相等,还需要考虑Store和Load的数据大小是否重叠。一个Store写入地址AA的4字节(AAA+3A+3),一条Load从A+2A+2读取2字节——虽然起始地址不同,但访问范围有重叠,应该触发转发。

  • 程序顺序:只有在程序顺序上位于Load之前的Store才参与匹配。Store Buffer使用ROB年龄标记来确定程序顺序关系。

  • 多重匹配:如果多个Store匹配同一地址,选择最年轻的(程序顺序上最近的)匹配Store进行转发。

32个条目的全相联CAM阵列的面积不大(远小于D-Cache),但其匹配延迟是存储器流水线的一部分——CAM查找与D-Cache Tag查找并行进行,在Tag比较完成的同时,Store Buffer的匹配结果也就绪,两者通过一个优先级MUX选择最终的数据源(Store Buffer转发优先于D-Cache数据)。

Speculative Disambiguation

存储器消歧(Memory Disambiguation)是乱序执行处理器中的一个关键问题。当一条Load指令在程序顺序上位于一条未完成地址计算的Store指令之后时,处理器面临一个困境:

  • 保守策略:等待所有之前的Store指令完成地址计算后,再检查是否有地址冲突。如果没有冲突,Load才能执行。这种策略安全但慢——Load指令必须等待所有之前Store的地址就绪。

  • 激进策略:假设Load与之前的Store没有地址冲突,立即执行Load。如果后来发现确实存在冲突(Store的地址与Load相同),则回滚并重新执行Load。

21264采用了激进策略的改进版本——通过一个称为Store Wait Map(SWM)的结构来指导投机决策。

Store Wait Map的工作原理。Store Wait Map是一个硬件表格,记录了哪些Load指令历史上曾经因为与之前的Store发生地址冲突而导致执行错误。其基本思想是:

  1. 默认情况下,所有Load指令都被假设与之前的Store没有地址冲突,可以立即执行(投机执行)。

  2. 当一条Store指令完成地址计算后,处理器检查是否有之前投机执行的Load与该Store的地址相同。如果有冲突,说明该Load读取了错误的数据,需要重新执行该Load及其依赖指令。

  3. 发生冲突后,处理器在Store Wait Map中记录该Load(使用Load的PC标识),标记该Load为"需要等待"。

  4. 下次执行到同一条Load指令(相同PC)时,发射逻辑查找Store Wait Map。如果该Load被标记为"需要等待",则它不会被投机执行,而是等待所有之前的Store完成地址计算后再执行。

硬件描述 10 — Store Wait Map的位向量结构与实现

Store Wait Map在21264中实现为一个由Store指令PC索引的位向量(bit vector)表格。与直觉相反,SWM的索引不是Load的PC,而是Store的PC。其位向量结构的关键在于:每个Store PC对应的条目是一个NN-bit的位向量,其中每一位标记一个特定的Load(通过Load PC的哈希值标识)是否曾经与该Store发生过地址冲突。

位向量的使用方法。SWM的工作流程如下:

  1. 冲突记录:当检测到一条Load(PC地址为PCL\text{PC}_L)与一条Store(PC地址为PCS\text{PC}_S)发生地址冲突时,使用PCS\text{PC}_S索引SWM表格,将对应条目中第h(PCL)h(\text{PC}_L)位设置为1(其中hh是一个哈希函数,将Load PC映射到位向量的某一位)。

  2. 等待判断:当一条新的Load指令即将被投机发射时,处理器查找当前所有未完成地址计算的Store在SWM中的条目,取这些条目中第h(PCL)h(\text{PC}_L)位的OR值。如果OR结果为1,说明该Load历史上曾与某个尚未解析的Store发生过冲突,Load不应被投机执行,而是等待那些Store的地址计算完成。

  3. 安全执行:如果OR结果为0,说明该Load没有历史冲突记录,可以安全地投机执行。

这种位向量结构的精妙之处在于:它将"哪条Load曾与哪条Store冲突"这一二维关系压缩为一个以Store PC索引的一维表格,每个条目用位向量编码与之冲突的Load集合。查询时只需要对相关Store条目的特定位进行OR操作。

存储预算与精度分析。假设SWM有256个条目(使用Store PC的低8位索引),每个条目为32-bit位向量(Load PC的低5位作为位向量索引),则总存储量为256×32=8,192256 \times 32 = 8{,}192 bits,约1 KB。这个存储量在21264的面积预算中是可以接受的。

位向量方案不可避免地存在别名问题(aliasing):不同的Store PC可能映射到同一个SWM条目,不同的Load PC可能映射到位向量的同一位。别名可能导致两种错误:

  • 假阳性(False Positive):一条从未发生冲突的Load被标记为"需要等待"。这会降低Load的投机执行率,增加不必要的等待延迟。

  • 假阴性(False Negative):理论上不会发生,因为真正的冲突总会被记录。但由于遗忘机制(下文详述),已清除的标记可能导致冲突再次发生。

遗忘机制与自适应清除。SWM的更新策略包含一个遗忘机制:被标记的位在经过一定次数的"无冲突"执行后会被清除,允许该Load再次尝试投机执行。这种遗忘机制是必要的,因为地址冲突可能是暂时性的——同一条Load指令在不同执行时(不同循环迭代、不同函数调用上下文)可能与Store冲突或不冲突。

遗忘的具体实现可以是:

  • 周期性清除:每隔一定数量的周期(或退休指令数),将SWM中的所有位清零。这是最简单的方案,但过于粗暴——可能导致已学习到的冲突信息被过早丢弃。

  • 计数器辅助清除:为SWM中的每一位关联一个小型饱和计数器(如2-bit)。每次投机执行成功(无冲突)时递减计数器,计数器到0时清除标记位。冲突发生时设置标记位并重置计数器到最大值。这种方案更加精细,但存储开销增加。

21264的具体遗忘策略未完全公开,但从设计论文的描述来看,它采用了某种自适应策略来平衡冲突学习速度和遗忘速度。

SWM的关键设计权衡是:

  • 表格大小:更大的SWM可以减少Store PC的哈希冲突,提高记录的精确度。但更大的表格消耗更多面积和功耗。

  • 位向量宽度:更宽的位向量可以减少Load PC的哈希冲突,但增加每个条目的存储量和查询时的OR逻辑宽度。

  • 遗忘速率:遗忘太快导致反复冲突和重放;遗忘太慢导致不必要的等待。

案例研究 5 — Store Wait Map的实际场景分析

以一个常见的编程模式来说明Store Wait Map的工作:

    // C代码:通过指针写入后立即读取
    void process(int *ptr, int *result) {
        *ptr = compute_value();      // Store S1 (PC = 0x1000)
        *result = *ptr + 1;          // Load L1 (PC = 0x1008), 读取ptr
    }

在这段代码中,Load L1依赖于Store S1(两者访问相同的地址*ptr),但这一依赖在地址计算完成之前无法确定。

第一次执行:处理器投机地假设L1和S1无冲突,在S1地址尚未就绪时就发射L1。L1从D-Cache读取*ptr的旧值(S1尚未写入),得到错误数据。当S1地址就绪后,检测到冲突,触发存储器顺序违规。SWM记录:在S1的SWM条目中设置与L1对应的位。L1及后续指令被重放。

第二次执行:当L1再次准备发射时,发射逻辑查找SWM,发现S1的条目中L1的对应位为1。L1被标记为"需要等待",不进行投机发射。L1等待S1地址计算完成,确认地址匹配后通过Store-to-Load转发获得正确数据。无违规,无重放。

后续执行:如果程序的调用模式改变,使得ptrresult指向不同地址,L1和S1不再冲突。SWM的遗忘机制在多次无冲突执行后清除标记,L1恢复投机执行。

这个例子说明了SWM的"学习-利用-遗忘"循环:首次冲突时付出一次重放代价(学习),后续执行避免重复冲突(利用),条件变化后解除不必要的等待(遗忘)。

存储器消歧违规的检测与恢复。当检测到Load-Store地址冲突时(称为存储器顺序违规,Memory Order Violation),21264需要恢复执行状态。恢复过程类似于分支预测失败的恢复:

  1. 违规的Load指令及其之后的所有指令(在程序顺序上)被标记为无效。

  2. 这些指令被从ROB中清除(或等待它们退休时不提交)。

  3. 前端从违规Load指令的PC重新取指。

  4. Store Wait Map中记录该Load的PC,防止未来再次错误投机。

这种恢复的代价较高(类似于分支预测失败的惩罚),但由于Store Wait Map的学习效果,同一条Load的重复违规概率很低。根据DEC的测量数据,在SPECint95基准测试中,存储器顺序违规的频率约为每千条Load指令不到1次——SWM有效地将大部分可能冲突的Load标记为"需要等待",避免了重复违规。

设计提示

Store Wait Map是一种基于历史的投机控制机制——处理器根据过去的行为来预测未来是否应该进行投机执行。这一思想后来在处理器设计中被广泛应用:

  • AMD的存储器消歧预测器(Memory Disambiguation Predictor)使用类似的机制来控制Load的投机执行。

  • Intel的Memory Disambiguator在Skylake及后续微架构中使用一种改进的预测方案。

  • 现代ARM核心中的Store-to-Load依赖预测器也遵循类似的"默认投机、历史学习"范式。

从更广泛的角度看,SWM体现了乱序处理器设计中的一个普遍原则:对于无法在执行前确定的信息(如地址冲突),先用投机方式假设最常见的情况(无冲突),然后在检测到错误时学习并调整后续的投机策略。

Load Hit/Miss Prediction

21264的另一个创新机制是Load命中/缺失预测(Load Hit/Miss Prediction)。这一机制与发射队列的投机唤醒(speculative wakeup)紧密相关。

问题背景。在乱序执行处理器中,当一条Load指令被发射到存储器流水线时,发射队列需要决定何时唤醒依赖于该Load结果的后续指令。有两种策略:

  • 保守唤醒:等待Load指令实际完成D-Cache访问、确认命中后,再唤醒后续指令。这保证了唤醒一定正确,但增加了有效的Load-use延迟——后续指令在Load完成后还需要等待唤醒和发射的延迟。

  • 投机唤醒:在Load被发射的同时,假设它将在已知的延迟内完成(即命中D-Cache),立即唤醒后续指令。这使得后续指令可以在Load完成的同一周期或下一个周期就开始执行,实现最短的Load-use延迟。但如果Load实际缺失了D-Cache,被唤醒的后续指令将读取到错误的数据(或者说,数据尚未就绪),必须被取消并重新发射。

21264采用了投机唤醒策略——当一条Load指令被发射时,发射队列乐观地假设该Load将命中D-Cache(3周期延迟),立即安排在3周期后唤醒所有依赖该Load结果的指令。

Load命中/缺失预测器。为了减少投机唤醒错误的频率,21264引入了一个Load命中/缺失预测器。这个预测器的作用是预测一条Load指令是否会命中D-Cache:

  • 如果预测命中:使用投机唤醒,在Load发射时就唤醒后续指令(假设3周期后数据就绪)。

  • 如果预测缺失:不使用投机唤醒,等待Load实际完成后再唤醒后续指令。这避免了因D-Cache缺失导致的错误唤醒和重发。

预测器的结构。Load命中/缺失预测器在实现上基于Load指令的PC地址进行预测。其核心结构是一个由Load PC索引的预测表,每个条目包含一个2-bit饱和计数器,记录该Load指令的D-Cache命中历史。

预测表的组织。预测表的条目数通常为2k2^k个(21264的具体大小未完全公开,根据面积分析估计为256–1024个条目)。每个条目的2-bit计数器按以下规则工作:

  • 计数器值为11或10:预测命中。Load使用投机唤醒,发射时即唤醒后续依赖指令。

  • 计数器值为01或00:预测缺失。Load不使用投机唤醒,等待实际Cache访问结果后再唤醒后续指令。

  • 每次Load命中D-Cache:计数器向"命中"方向递增(最大到11)。

  • 每次Load缺失D-Cache:计数器向"缺失"方向递减(最小到00)。

基于PC预测的合理性。使用Load PC作为预测索引是合理的,因为同一条Load指令在多次执行时的D-Cache命中率通常具有较强的时间相关性。这种相关性来自两个方面:

  1. 空间局部性:如果一条Load指令访问的数据在当前工作集内(如循环中访问的数组元素),它在多次执行中都倾向于命中D-Cache。相反,如果它访问的数据不在工作集内(如链表遍历中访问分散的节点),则多次执行都倾向于缺失。

  2. 缓存行驻留时间:一条频繁执行的Load指令(在热循环中)访问的数据往往被频繁重用,Cache行被替换的概率低,因此持续命中。而不频繁执行的Load(如异常处理路径中的Load)访问的数据可能已经被替换,更可能缺失。

预测与投机唤醒的集成。Load命中/缺失预测器的输出在Load指令被发射时使用。发射逻辑根据预测结果决定唤醒策略:

  • 预测命中:在Load发射的同一周期,发射队列将所有依赖该Load结果的指令的相关源标记为"将在T+3周期就绪"(T为Load发射周期,3为D-Cache访问延迟)。这些依赖指令在T+3周期被唤醒,T+4周期可以被选择发射。

  • 预测缺失:发射队列不对依赖指令进行投机唤醒。依赖指令的源保持"未就绪"状态,直到Load的结果实际写入物理寄存器文件后,才通过正常的唤醒广播唤醒依赖指令。

预测准确率与性能分析。Load命中/缺失预测器的准确率取决于程序的Cache行为特征。对于D-Cache命中率高的程序(如典型的SPECint95基准测试,命中率>>90%),大部分Load被正确预测为命中,投机唤醒在绝大多数情况下是正确的。只有少数确实缺失的Load被错误地预测为命中,导致投机唤醒失败。

Load命中/缺失预测器的特别价值在于处理工作集过渡(working set transition)阶段。当程序从一个工作集切换到另一个时(如进入一个新的循环、访问新的数据结构),D-Cache缺失率会暂时升高。如果没有命中/缺失预测器,发射队列会对所有Load进行投机唤醒,导致大量错误唤醒和重放,浪费执行带宽。命中/缺失预测器在学习到新的缺失模式后,会禁止对这些Load的投机唤醒,减少无效的重放次数。

投机唤醒在D-Cache命中与缺失两种情况下的行为。命中时,后续指令通过旁路获得数据,实现最短延迟;缺失时,已唤醒的后续指令必须被取消并在数据到达后重新发射。
投机唤醒在D-Cache命中与缺失两种情况下的行为。命中时,后续指令通过旁路获得数据,实现最短延迟;缺失时,已唤醒的后续指令必须被取消并在数据到达后重新发射。

错误唤醒的恢复。当投机唤醒失败(Load缺失D-Cache)时,已经被唤醒并可能已经开始执行的后续指令需要被重放(replay)。21264的重放机制如下:

  1. 当Load在D-Cache查找阶段确定缺失时,产生一个重放信号(replay signal)。

  2. 所有已经被该Load的投机唤醒信号唤醒、且尚未完成执行的指令被标记为无效。

  3. 这些指令重新进入发射队列的"等待"状态,它们的源就绪位被清除。

  4. 当Load的数据最终从L2 Cache到达并写入物理寄存器文件时,这些指令被真正唤醒,可以再次被发射执行。

重放的代价。每次错误唤醒和重放都浪费了功能单元的执行带宽和发射队列的仲裁带宽。在D-Cache缺失率较高的程序中(如大数据集的随机访问模式),频繁的错误唤醒会显著降低性能。Load命中/缺失预测器通过预测性地禁止可能缺失的Load的投机唤醒,可以减少约一半的错误重放次数。

重放的级联效应。投机唤醒错误的影响不限于直接依赖缺失Load的指令——它可能产生级联(cascading)效应。考虑以下依赖链:

    LD   R1, [R10]       // Load L1: 缺失D-Cache
    ADD  R2, R1, R3      // I2: 直接依赖L1
    MUL  R4, R2, R5      // I3: 间接依赖L1(通过I2)
    SUB  R6, R4, R7      // I4: 间接依赖L1(通过I2, I3)

当Load L1被发射并触发投机唤醒时,I2在T+3周期被唤醒,I3在T+4周期被唤醒(因为它依赖I2,而I2预计在T+4周期产生结果),I4在T+5周期被唤醒。如果L1缺失D-Cache,不仅I2需要重放,I3和I4也需要重放——因为它们的源操作数值都是无效的(基于L1的错误旁路值计算得到)。

在最坏情况下,一个缺失Load可能导致数十条依赖链上的指令被错误唤醒和重放,浪费大量的执行带宽。这就是Load命中/缺失预测器的核心价值——通过在源头(Load发射时)就抑制不必要的投机唤醒,避免了整条依赖链的级联重放。

硬件描述 11 — 重放机制的实现选择

21264的重放机制需要能够精确识别哪些指令需要重放。主要有两种实现策略:

方案1——选择性重放:只重放直接和间接依赖于缺失Load的指令。这需要维护指令间的依赖关系信息,在缺失发生时沿依赖图传播"取消"信号。实现复杂但高效——不依赖缺失Load的指令不受影响。

方案2——年龄截断重放:重放缺失Load之后(程序顺序上)的所有指令,无论它们是否依赖该Load。实现简单(只需比较ROB中的年龄标记),但过于保守——很多无关指令也被不必要地重放。

21264采用的方案介于两者之间:它通过发射队列中的源就绪位来跟踪依赖关系。当缺失Load被检测到时,所有在发射队列中等待该Load结果的指令(以及已经被错误唤醒但尚未完成执行的指令)的源就绪位被清除。这些指令重新进入等待状态,直到Load数据到达后被真正唤醒。已经完成执行的无关指令不受影响。

性能分析 6 — 投机唤醒的收益分析

投机唤醒的收益可以通过比较"有投机唤醒"和"无投机唤醒"两种情况的Load-use延迟来量化。

无投机唤醒:Load-use延迟 = D-Cache访问延迟(3周期)+ 唤醒延迟(1周期)+ 发射延迟(1周期)= 5周期

有投机唤醒(且命中D-Cache):Load-use延迟 = D-Cache访问延迟(3周期)= 3周期。后续指令在Load完成的同周期通过旁路获得数据。

对于D-Cache命中率为95%的典型程序,有效的Load-use延迟为:

Leff=0.95×3+0.05×(3+LL2)=2.85+0.05×LL2L_{\text{eff}} = 0.95 \times 3 + 0.05 \times (3 + L_{\text{L2}}) = 2.85 + 0.05 \times L_{\text{L2}}

其中LL2L_{\text{L2}}为L2 Cache的访问延迟。相比之下,无投机唤醒的有效延迟为:

Lno-spec=0.95×5+0.05×(5+LL2)=4.75+0.05×LL2L_{\text{no-spec}} = 0.95 \times 5 + 0.05 \times (5 + L_{\text{L2}}) = 4.75 + 0.05 \times L_{\text{L2}}

在命中情况下,投机唤醒减少了约2个周期的Load-use延迟,这对整数密集的代码(其关键路径通常由Load-use依赖链主导)具有显著的性能提升效果。

D-Cache的组织。21264的D-Cache为64 KB、2-way set-associative,行大小为64字节。D-Cache支持每周期2个Load操作或1个Load和1个Store操作(双端口),通过使用双倍频率(double-pumped)的SRAM实现等效双端口。

D-Cache采用写回(write-back)策略,使用MOESI协议维护与外部Cache的一致性。写入D-Cache的操作仅在Store指令提交时才从Store Buffer转移到D-Cache,保证了精确异常语义——推测性Store的数据不会污染Cache。

硬件描述 12 — 21264 D-Cache的地址空间管理

21264的D-Cache使用虚拟地址索引、物理地址Tag(Virtually Indexed, Physically Tagged, VIPT)方案。这一方案结合了虚拟地址索引的速度优势(不需要等待TLB翻译即可开始读取数据)和物理地址Tag的正确性保证(避免同义问题/synonym problem)。

对于21264的64 KB 2-way D-Cache,索引位宽为log2(64KB/2)=15\log_2(64\text{KB}/2) = 15位,而Alpha架构的页大小为8 KB(log2(8KB)=13\log_2(8\text{KB}) = 13位)。由于索引位(15位)超过了页内偏移位(13位),高2位的索引取决于虚拟-物理地址翻译结果,可能产生同义问题。21264通过操作系统级的页着色(page coloring)策略来避免这一问题——操作系统保证映射到同一物理页的虚拟页在这2位上具有相同的值。

D-Cache的双端口实现

21264每周期可以执行2条存储器指令,这要求D-Cache提供等效的双端口访问能力。然而,真正的双端口SRAM(每个存储单元有两套独立的位线和字线)面积约为单端口SRAM的2–3倍,在64 KB的容量下这一面积开销是不可接受的。

21264采用了双倍频率(double-pumped)方案来实现等效双端口。具体地,D-Cache的SRAM阵列运行在核心频率的2倍——如果核心频率为500 MHz,D-Cache SRAM运行在1 GHz。在一个核心时钟周期的前半周期完成第一次访问,后半周期完成第二次访问,对核心看来等效于在一个周期内完成了两次访问。

双倍频率方案的约束。这种方案对SRAM的访问延迟有严格要求——每次访问必须在半个核心周期内完成。在500 MHz核心频率下,半周期为1 ns,这要求D-Cache SRAM的行选择、数据读出和感应放大完全在1 ns内完成。这一约束进一步限制了D-Cache的关联度——2-way已经是在这一时序下可实现的最高关联度。

读写冲突处理。双倍频率方案中,如果两次访问中一次是Load、一次是Store,它们访问不同的Cache行,没有冲突。如果两次访问指向同一Cache行的同一地址(例如一条Store和一条Load访问相同地址),需要特殊处理:Store写入通过Store Buffer延迟执行,Load可以通过Store-to-Load转发获得数据。如果两次都是Load且指向不同地址,双倍频率方案可以正常服务。

D-Cache的写入策略。21264的D-Cache使用写回(write-back)策略:Store数据只写入D-Cache,不同时写入L2 Cache或主存。脏数据只在被替换或一致性协议要求时才写回下一级存储。写回策略的优势在于减少了总线流量——如果一个Cache行被多次写入后才被替换,所有中间写入都只发生在D-Cache内部,仅最终版本需要写回。

D-Cache的写分配策略。21264对D-Cache写入使用写分配(write-allocate)策略:当Store指令缺失D-Cache时,首先从L2 Cache加载该Cache行到D-Cache,然后在D-Cache中进行写入。这与非写分配(no-write-allocate)策略形成对比——后者在写缺失时直接将数据写到L2 Cache,不加载到D-Cache。

写分配策略的优势在于后续对同一行的Store或Load可以在D-Cache中命中,利用了空间和时间局部性。但缺点是写缺失时需要先从L2加载整行数据(即使Store只写入行中的一小部分),增加了总线流量。这种权衡在大多数工作负载下是有利的——程序通常会在写入后不久再次访问同一数据。

设计提示

21264的D-Cache设计体现了在有限资源下追求最佳性能的务实策略。64 KB的容量在当时是同类处理器中最大的,通过降低缺失率来弥补缺少片上L2 Cache的劣势。2-way的关联度是频率约束下的最优选择——direct-mapped的缺失率太高,4-way以上的关联度在目标频率下无法实现单周期Tag比较。双倍频率的等效双端口方案是面积和性能的最佳折衷——真双端口面积翻倍不可接受,单端口则限制存储器带宽。这些设计选择中的每一个都不是孤立的最优解,而是在整体系统约束下的全局最优权衡。

TLB结构

21264的地址翻译使用全相联TLB(Translation Lookaside Buffer)。21264提供了两个独立的TLB:

  • I-TLB(指令TLB):128个条目,全相联。用于取指地址的虚拟到物理翻译。

  • D-TLB(数据TLB):128个条目,全相联。用于Load/Store地址的虚拟到物理翻译。

全相联TLB的实现。全相联TLB使用CAM(Content-Addressable Memory)实现——所有128个条目同时将输入的虚拟页号(VPN)与自身存储的VPN进行比较,匹配的条目输出对应的物理页号(PPN)。全相联结构不存在冲突缺失(conflict miss),每个条目可以映射任意的虚拟页面,TLB利用率最高。

128个全相联条目在0.35 μm工艺下的面积约等于8 KB的SRAM——主要开销来自128个CAM比较器。但全相联TLB的缺失率远低于同规模的组相联TLB,对于128个条目来说,全相联方案的面积开销是值得的。

TLB缺失处理。Alpha架构的TLB缺失由PALcode软件处理(而非硬件页表遍历器)。当发生TLB缺失时:

  1. 处理器产生一个TLB缺失异常,陷入PALcode处理程序。

  2. PALcode在系统内存中的页表结构中查找缺失的虚拟页对应的物理页映射。

  3. PALcode使用特权指令将新的TLB条目写入硬件TLB。

  4. 异常返回,被中断的指令重新执行。

这种软件管理TLB的方案比硬件页表遍历器更灵活(支持不同的页表格式),但TLB缺失的延迟也更高(需要数十到数百个周期,取决于PALcode的复杂度和内存层次的命中情况)。

大页支持。Alpha架构支持多种页面大小(8 KB、64 KB、512 KB、4 MB),21264的TLB可以混合存储不同大小的页面映射。大页可以显著降低TLB缺失率——一个4 MB的大页可以覆盖512个8 KB小页的地址范围,对于大数据集应用尤其有益。

TLB容量的充分性分析。128个TLB条目在8 KB页面大小下可以覆盖128×8KB=1MB128 \times 8\text{KB} = 1\text{MB}的地址空间。对于工作集小于1 MB的程序(大多数整数基准测试),TLB缺失率可以忽略。对于大工作集的科学计算应用,可以使用64 KB或更大的页面——128个条目在64 KB页面下覆盖128×64KB=8MB128 \times 64\text{KB} = 8\text{MB},在4 MB大页下覆盖128×4MB=512MB128 \times 4\text{MB} = 512\text{MB}

I-TLB与D-TLB的分离。21264使用独立的I-TLB和D-TLB,各128个条目。分离的TLB有以下优势:

  • 消除结构冲突:取指地址翻译和数据地址翻译可以在同一周期内并行进行,无需仲裁共享TLB的访问端口。

  • 减少冲突缺失:指令和数据地址的访问模式不同——指令访问通常具有很强的空间局部性(顺序取指),而数据访问可能更分散。分离的TLB避免了两种访问模式互相干扰。

  • 更简单的时序:每个TLB只需要一个读端口(一个CAM比较端口),时序比共享TLB的双端口设计更简单。

存储器访问的完整流水线

将上述各组件综合起来,21264的存储器访问经过以下流水线阶段。图图 40.6展示了Load和Store指令的完整流水线时序。

21264存储器访问流水线时序。Load经过发射、地址计算、TLB/Cache访问、Tag比较和写回五个阶段。Store在退休时才写入D-Cache。投机唤醒在Load发射时发出。
21264存储器访问流水线时序。Load经过发射、地址计算、TLB/Cache访问、Tag比较和写回五个阶段。Store在退休时才写入D-Cache。投机唤醒在Load发射时发出。

Load指令的完整流程:

  1. Stage 4(发射):Load指令从整数发射队列发射,同时查询Load命中/缺失预测器。如果预测命中,向发射队列广播投机唤醒信号。

  2. Stage 5(地址计算):使用整数ALU计算有效地址(base+offset)。计算出的地址被送入D-TLB进行虚拟到物理翻译,同时使用虚拟地址的索引位开始D-Cache SRAM的行选择。

  3. Stage 6(D-Cache访问):D-Cache读出两个Way的数据和Tag。TLB输出物理地址,Tag比较器确定命中Way。Store Buffer同时进行地址匹配查找。

  4. Stage 7(数据选择与写回):根据Tag匹配结果(或Store Buffer转发结果)选择最终数据,写入物理寄存器文件。如果D-Cache缺失,产生缺失请求送入Cbox。

Store指令的完整流程:

  1. Stage 4(发射):Store指令从发射队列发射。注意Store指令可能分为两部分发射——地址计算部分和数据准备部分可以独立发射。

  2. Stage 5(地址计算):计算Store的有效地址,结果写入Store Buffer的对应条目。

  3. Stage 5–6(存储器消歧检查):Store地址就绪后,检查是否有在程序顺序上位于该Store之后的Load已经投机执行,且与该Store地址冲突。如果发现冲突,触发存储器顺序违规恢复。

  4. 退休时:Store指令在ROB头部提交时,Store Buffer中的数据被转移到D-Cache的写队列,最终写入D-Cache。

外部Cache接口(Cbox)

21264的外部Cache接口单元(Cbox)管理L1 Cache与片外L2 Cache之间的数据传输。由于21264没有集成片上L2 Cache,Cbox的设计对整体存储器性能有至关重要的影响。

Cbox的结构与功能

Cbox连接在L1 I-Cache/D-Cache与片外系统总线之间,负责以下功能:

  • 缺失处理:当L1 Cache发生缺失时,Cbox生成对L2 Cache的请求,管理缺失请求队列。

  • 写回管理:当D-Cache中的脏行(dirty line)被替换时,Cbox将脏数据写回L2 Cache。

  • 一致性协议:Cbox实现MOESI协议的外部接口,响应来自其他处理器或I/O设备的探测(snoop)请求。

  • 预取控制:Cbox支持硬件预取和软件预取指令(FETCHFETCH_M)。

缺失处理队列(Miss Address File, MAF)。21264的Cbox包含一个8条目的MAF(Miss Address File),记录当前未完成的L1 Cache缺失请求。MAF的功能包括:

  • 合并(Merging):如果多个缺失请求指向同一Cache行,MAF将它们合并为一个对L2 Cache的请求,避免重复的外部总线事务。

  • 跟踪:MAF跟踪每个未完成请求的状态(已发送、等待响应、数据到达等)。

  • 唤醒:当L2 Cache数据到达时,MAF通知D-Cache控制器将数据填充到Cache行中,同时唤醒等待该数据的Load指令。

8个MAF条目意味着21264最多可以同时处理8个未完成的L1 Cache缺失。这一并行度(Memory-Level Parallelism, MLP)对于隐藏L2 Cache延迟至关重要。

硬件描述 13 — MAF与存储器级并行的关系

存储器级并行(Memory-Level Parallelism, MLP)指的是处理器同时维持多个未完成的存储器访问的能力。21264的8条目MAF允许最多8个缺失同时在处理中,这对于性能的影响可以通过以下分析理解:

假设L2 Cache访问延迟为L2=15L_2 = 15个周期。如果每次只能处理1个缺失(MLP = 1),则连续8个L1缺失的总延迟为8×15=1208 \times 15 = 120个周期。如果8个缺失可以完全并行处理(MLP = 8),总延迟仅为1515个周期——减少了7×7 \times

在实际执行中,MLP的有效值取决于程序的缺失模式。对于指针追踪型代码(如链表遍历),每个缺失依赖于前一个缺失的结果(下一个节点的地址来自当前节点的指针字段),MLP接近1,8个MAF条目大部分空闲。对于数组遍历型代码(如矩阵运算中的流式访问),多个独立的缺失可以同时发出,MLP接近MAF容量的上限。

21264的80条目ROB也是维持高MLP的关键——ROB容量决定了处理器在不提交的情况下能够向前推进多远。如果ROB太小(如只有20条目),处理器在遇到第一个缺失后很快就会因为ROB满而暂停前端,无法发出更多的缺失请求。80条目的ROB允许处理器在一个长延迟缺失等待期间继续执行后续的无关指令,发出更多的缺失请求。

Victim Buffer。21264在Cbox中包含一个8条目的Victim Buffer,用于暂存从L1 Cache被替换出的Cache行。当一个缺失导致Cache行替换时,被替换的行首先被移入Victim Buffer而非立即写回L2。如果后续的访问恰好命中Victim Buffer中的行,可以直接从Victim Buffer取回数据并交换到L1 Cache中,避免一次完整的L2 Cache访问。Victim Buffer对于冲突缺失(conflict miss)频繁的2-way Cache尤其有效。

Victim Buffer的命中处理。当一次D-Cache缺失同时查找Victim Buffer并命中时,处理器执行一次交换操作:Victim Buffer中命中的行被送回D-Cache,同时D-Cache中被替换出的行被送入Victim Buffer。这种交换操作的延迟约为5–8个周期,远小于L2 Cache的15–20个周期延迟。

对于21264的2-way set-associative D-Cache,Victim Buffer特别重要。2-way的低关联度意味着同一个Cache组只能容纳2个不同的Cache行——如果程序访问映射到同一组的3个或更多数据块(ping-pong pattern),其中一个块必须不断被替换和重新加载,导致高冲突缺失率。Victim Buffer提供了额外的8行缓冲空间,有效增加了局部关联度,将部分冲突缺失转化为Victim Buffer命中(延迟5–8周期)而非L2缺失(延迟15–20周期)。

Cache一致性协议

21264使用MOESI协议维护多处理器系统中的Cache一致性。MOESI协议在经典的MESI协议基础上增加了一个Owned状态,提供了一种优化脏数据共享的机制。

MOESI的五种状态。每个D-Cache行可以处于以下5种状态之一:

  • Modified(M):该行在本Cache中被修改过,且是唯一的有效副本。主存中的数据已过期。写回义务由本Cache承担。

  • Owned(O):该行在本Cache中被修改过(相对于主存),但其他Cache中可能存在该行的Shared副本。本Cache负责提供最新数据——如果其他处理器请求该行,本Cache需要直接提供数据(而非从主存读取过期数据)。

  • Exclusive(E):该行在本Cache中是唯一副本,且与主存一致。可以在不通知其他Cache的情况下直接修改为Modified状态。

  • Shared(S):该行可能在多个Cache中存在副本。本地写入需要先获取独占权限(发送Invalidate事务)。

  • Invalid(I):该行无效。

Owned状态的优势。MOESI协议相比MESI的关键改进在于Owned状态允许脏数据在不写回主存的情况下与其他处理器共享。在MESI协议中,当一个处理器持有Modified行时,如果另一个处理器请求读取该行,Modified持有者必须将数据写回主存,然后两者都变为Shared状态。在MOESI中,Modified持有者可以直接将数据转发给请求者,自己从Modified变为Owned,请求者获得Shared副本——整个过程不需要写回主存。这在多处理器系统中减少了主存带宽的消耗。

Snoop处理。21264的Cbox包含Snoop逻辑,监听系统总线上其他处理器发出的访问请求。当检测到一个地址匹配本地D-Cache中某行时,Snoop逻辑根据当前行状态和请求类型执行相应操作——可能包括将数据转发给请求者、使本地副本失效、或将状态从Modified/Exclusive降级为Shared。

Snoop操作的类型。21264需要处理以下几种来自其他处理器的Snoop请求:

  • Read(读取请求):另一个处理器要读取某地址的数据。如果本地Cache持有Modified或Owned副本,需要将数据转发给请求者,并将本地状态变更为Owned(允许请求者获得Shared副本)。如果本地Cache处于Exclusive状态,需要降级为Shared。

  • Read-with-Intent-to-Modify(带修改意图的读取):另一个处理器要写入某地址。如果本地Cache持有该行的任何有效副本,都必须失效(变为Invalid),因为请求者将获得独占的Modified副本。如果本地持有修改过的数据(Modified或Owned),还需要在失效前将数据转发给请求者。

  • Invalidate(失效请求):另一个处理器已经持有该行的Shared副本,现在要升级为Modified。本地的Shared副本必须失效。

Snoop延迟与性能影响。Snoop处理可能与处理器核心的正常D-Cache访问产生冲突——两者可能在同一周期尝试访问D-Cache的Tag阵列。21264通过在D-Cache Tag阵列上设置一个仲裁器来解决这一冲突:如果Snoop请求和核心访问同时到达,核心访问优先(因为它在流水线关键路径上),Snoop请求被延迟一个周期。这一策略保证了核心流水线不受Snoop处理的干扰。

在单处理器系统中,Snoop逻辑基本空闲。在多处理器系统中,Snoop流量取决于处理器之间的数据共享程度。对于共享数据少的工作负载(如独立的计算任务),Snoop干扰可忽略。对于共享数据密集的工作负载(如数据库事务处理),频繁的Snoop可能增加D-Cache的争用,导致核心访问延迟增加。

Snoop过滤。21264的片上L2 Tag(前文提到)在Snoop处理中发挥了重要作用:来自总线的Snoop请求首先查找片上L2 Tag。如果L2 Tag显示该地址不在本处理器的Cache层次中,Snoop可以立即返回"不命中"响应,无需查找L1 D-Cache的Tag。这种L2 Tag充当了Snoop过滤器(Snoop Filter)的功能,减少了L1 D-Cache Tag的Snoop查找次数,降低了Snoop与核心访问之间的冲突概率。

EV6系统总线

21264使用EV6总线协议与片外L2 Cache和系统内存通信。EV6总线的关键特性包括:

  • 分离事务(Split-Transaction):地址阶段和数据阶段是分离的,允许多个事务同时在总线上流水线化进行。这比传统的独占式总线(一次只能进行一个事务)有更高的有效带宽。

  • 点对点连接:EV6总线支持处理器与北桥芯片之间的点对点连接,减少总线仲裁延迟。

  • 高带宽:EV6总线在双倍数据速率(DDR)模式下运行,有效带宽可达数GB/s。在600 MHz版本的21264上,系统总线带宽约为3.2 GB/s。

L2 Cache的配置。21264的片外L2 Cache通过专用总线连接,支持1 MB到16 MB的容量配置。L2 Cache使用direct-mapped组织(某些系统配置使用2-way),由专用的L2 Cache控制器SRAM实现。L2 Cache的访问延迟(从处理器发出请求到数据返回)约为12–20个核心时钟周期,取决于L2 Cache SRAM的速度和总线频率。

L2 Cache Tag的存储位置。21264的一个独特设计是L2 Cache的Tag信息存储在芯片上(on-chip),而L2 Cache的数据存储在芯片外(off-chip SRAM)。这一分离设计有重要的性能意义:

  • 快速Tag查找:L2 Tag存储在片上使得Tag比较可以在1–2个周期内完成。处理器可以快速确定一个地址是否命中L2 Cache,而不需要等待片外SRAM的访问延迟。

  • 缺失快速检测:如果L2 Tag比较结果为缺失,处理器可以立即发起对主存的请求,而不需要等待L2数据SRAM的无意义访问。这减少了L2缺失的有效延迟。

  • 一致性探测优化:来自系统总线的Snoop请求可以通过查找片上L2 Tag快速确定该行是否在本处理器的L2 Cache中,而不需要访问片外SRAM。

片上L2 Tag的大小取决于L2 Cache的配置。对于4 MB的direct-mapped L2 Cache(64字节行大小),Tag条目数为4MB/64B=65,5364\text{MB}/64\text{B} = 65{,}536个。每个Tag约20位(物理地址位 - 索引位 - 偏移位)+ 状态位,总存储约为65,536×2419265{,}536 \times 24 \approx 192 KB。这一存储需求不大,但足以影响芯片面积分配。

预取支持。21264支持两种预取机制:

  • 软件预取:Alpha ISA定义了FETCH(预取读)和FETCH_M(预取写/独占)指令。这些指令是编译器或程序员显式插入的提示(hint),告诉处理器即将需要某个地址的数据。Cbox收到预取请求后,如果该地址未在L1 D-Cache中命中,会发起对L2 Cache的请求,将数据预加载到L1中。

  • 硬件流式预取:21264的Cbox包含简单的硬件预取逻辑,能够检测顺序访问模式(如数组遍历),在检测到连续的Cache行缺失后自动预取后续的Cache行。

预取机制对于数据密集型工作负载(如科学计算、数据库扫描)的性能至关重要——它允许处理器在数据实际被需要之前就将其加载到L1 Cache中,将L2延迟从关键路径上隐藏。

性能分析 7 — 存储器层次的有效延迟分析

21264的存储器层次延迟可以总结为:

层次延迟(周期)典型命中率
L1 D-Cache390%–95%
Victim Buffer5–82%–5%
L2 Cache(片外)12–2080%–95%
主存储器100–200

对于典型的整数基准测试,有效的平均Load延迟约为:

Lavg=0.92×3+0.03×6+0.04×16+0.01×1505.0周期L_{\text{avg}} = 0.92 \times 3 + 0.03 \times 6 + 0.04 \times 16 + 0.01 \times 150 \approx 5.0\text{周期}

其中假设L1命中率92%、Victim Buffer命中率3%、L2命中率4%、主存访问率1%。投机唤醒将命中情况下的Load-use延迟从5周期降到3周期,使有效延迟进一步降低。

退休与异常处理

ROB结构与退休机制

21264的ROB(Re-Order Buffer)包含80个条目,采用循环缓冲区组织。每个ROB条目记录以下信息:

  • 指令PC:用于异常处理时确定异常点。

  • 指令类型:整数、浮点、存储器访问、分支等。

  • 目标物理寄存器编号:该指令结果写入的物理寄存器。

  • 旧物理寄存器编号:该指令覆盖的逻辑寄存器之前映射的物理寄存器,用于提交时释放。

  • 完成状态位:标识该指令是否已执行完毕。

  • 异常状态:标识该指令是否产生了异常(如页故障、算术溢出)。

  • 分支信息:对于分支指令,记录预测方向和实际方向,以及可能需要的恢复信息。

21264的指令退休机制遵循标准的顺序提交模型。每周期最多可以提交(retire)11条指令——这个看似不对称的退休带宽(取指/发射宽度为4,但退休宽度高达11)是一个重要的设计决策。

宽退休带宽的原因。退休带宽之所以远大于发射带宽,是因为ROB容量(80条目)相对于发射宽度来说是有限的资源。如果退休带宽等于发射带宽(4条/周期),那么在持续执行的情况下ROB永远不会填满(稳态下每周期发射4条、退休4条)。但在实际执行中,由于D-Cache缺失、分支预测失败恢复等原因,退休可能暂时被阻塞——例如,当ROB头部的一条Load指令等待L2 Cache数据时,它之后的所有指令(即使已经执行完毕)都无法提交。当数据到达后,ROB中可能有数十条已完成的指令需要尽快退休以释放ROB空间和物理寄存器。宽退休带宽允许在这种情况下快速"排空"ROB中积压的已完成指令,避免ROB满导致的前端暂停。

物理寄存器的释放。当一条指令提交时,其ROB条目中记录的旧映射(old physical register)被释放回空闲列表。空闲列表的管理使用循环FIFO结构:退休时将旧物理寄存器编号推入FIFO尾部,重命名时从FIFO头部弹出新的物理寄存器编号。21264的退休宽度为11,意味着空闲列表每周期最多需要接受11个物理寄存器的释放——这要求空闲列表提供11个写端口,这在硬件实现上是一个不小的挑战。

Store的提交与D-Cache写入。Store指令在提交时,其数据从Store Buffer转移到D-Cache的写队列中。21264的Store Buffer容量为32条目,足以覆盖D-Cache写入的延迟。Store的提交顺序严格按照程序顺序,保证了存储器一致性模型的正确性。

精确异常的实现

21264支持精确异常:当ROB头部的一条指令检测到异常时(如页故障、算术异常),该指令及其之后的所有指令都不提交,处理器保存异常点的精确状态并跳转到PALcode异常处理程序。由于21264使用PALcode软件层处理异常,硬件只需要保存基本的异常信息(异常类型、异常PC)和跳转到PALcode入口地址。

异常的类型。21264处理的异常类型包括:

  • 同步异常:由指令执行本身触发的异常,如页故障(TLB缺失或权限违规)、非法指令、算术异常(浮点溢出/下溢/除零等)、对齐异常。

  • 异步异常(中断):由外部事件触发,如I/O设备中断、定时器中断、处理器间中断(IPI)。异步异常在ROB头部指令提交后的下一个提交点被响应。

  • PALcode调用CALL_PAL指令显式请求特权服务,类似于系统调用。

异常的延迟检测。由于乱序执行,异常可能在指令执行时就被检测到,但不能立即响应——必须等到该指令到达ROB头部(即将提交)时才处理。在等待期间,异常信息被记录在ROB条目中。如果在该指令到达ROB头部之前,有更早的指令也产生了异常,则优先处理更早指令的异常(按程序顺序)。如果异常指令之前有分支预测失败被检测到,该异常指令可能位于错误路径上,其异常应被忽略。

分支预测失败的恢复

21264在分支预测失败时的恢复策略采用WALK恢复方案。当BRU(Branch Resolution Unit,即ALU 3)检测到预测失败时,恢复过程分为以下几个阶段:

阶段1——前端重定向(1周期)。一旦BRU确定预测方向错误,正确的分支目标地址被计算出来。这个地址被立即送往取指单元,Line Predictor使用此地址开始新一轮取指。前端被重定向到正确路径。

阶段2——错误路径指令清除(1–2周期)。21264使用分支掩码(branch mask)机制来识别和清除错误路径上的指令。每条进入流水线的指令携带一个标识符,标记它是在哪个分支预测"之后"被取回的。当分支解析失败时,所有携带该分支标识的指令(及其之后分支的指令)被标记为无效:

  • 发射队列中尚未发射的错误路径指令被直接清除,释放队列条目。

  • 正在执行中的错误路径指令的结果被忽略(不写入寄存器文件或不影响架构状态)。

  • ROB尾指针回退到分支指令之后的位置,错误路径上的ROB条目被逻辑释放。

阶段3——映射表恢复(可变延迟,2–8+周期)。这是恢复过程中最耗时的阶段。21264通过WALK恢复从ROB中逆序遍历,撤销错误路径上每条指令的重命名操作。每个周期可以恢复多个映射条目(取决于恢复逻辑的带宽)。恢复延迟取决于错误路径上分配了目标寄存器的指令数量。

阶段4——恢复完成后重新开始。映射表恢复完成后,新从正确路径取回的指令可以进入解码和重命名阶段。在恢复期间,前端可能已经取回了若干正确路径的指令并缓冲在解码队列中,恢复完成后这些指令可以立即开始重命名,减少了有效惩罚。

21264的预测失败惩罚。综合取指重定向延迟和映射表恢复延迟,21264的分支预测失败惩罚约为7–10个周期。具体惩罚取决于错误路径上的指令数量——错误路径越长(即分支在流水线中停留越久才被解析),需要撤销的指令越多,WALK恢复越慢。在最好情况下(错误路径上只有很少指令),惩罚约7个周期;在较差情况下(错误路径上接近ROB容量的指令),惩罚可达15个周期或更多。

在600 MHz的频率下,7–10周期的惩罚对应约12–17 ns的绝对延迟。结合Tournament预测器约4%–6%的误预测率,分支预测失败导致的IPC损失约为5%–10%。

硬件描述 14 — 分支掩码机制的实现

分支掩码是21264管理推测执行深度的机制。处理器为每个未解析的分支分配一个掩码位(mask bit)。21264支持同时在飞(in-flight)的未解析分支数量有一个上限(约4–8个,具体值由掩码位宽决定)。

每条进入流水线的指令携带一个掩码向量,记录该指令依赖于哪些未解析分支的预测。当一个分支被正确解析时,对应的掩码位被释放,可以被新的分支使用。当一个分支预测失败时,所有掩码向量中包含该分支位(或更年轻分支位)的指令都被标记为无效。

这种掩码机制比逐条扫描ROB来判断指令是否在错误路径上更加高效——它通过一次位操作就能识别所有需要清除的指令。但掩码位的数量限制了同时在飞的分支数——如果掩码位用尽,前端必须暂停,等待最老的未解析分支被解析后释放掩码位。

案例研究 6 — 21264的性能分析

21264在1998年发布时,以466 MHz的频率在SPECint95基准测试中得分30.3、SPECfp95得分54.5,均为当时最高。到600 MHz版本发布时,SPECint95得分达到39.3。

21264的性能优势来自多个微架构特性的协同作用:

  • 高IPC:4-wide乱序执行、80条目的大指令窗口、高精度Tournament分支预测器共同贡献了约1.5–2.0的典型IPC(SPECint95),这在当时的处理器中是最高的。

  • 高频率:7级短流水线(相比于同时代Intel P6的12级、Pentium 4的20级)使得21264在每级的工作量较少,有利于实现高时钟频率。

  • 存储器效率:64 KB的L1 I-Cache和D-Cache显著降低了一级缺失率;投机消歧和Load命中/缺失预测减少了存储器访问的有效延迟。

  • Cluster化执行:双Cluster设计允许4-wide整数发射在可接受的面积和延迟约束下实现。

然而,21264也存在一些限制:

  • 缺少片上L2 Cache:在数据集超过L1 Cache容量的工作负载中,21264必须依赖片外L2 Cache(延迟约15–20个周期),性能显著下降。

  • 发射队列较小:20条目的整数发射队列在指令窗口较大时可能成为瓶颈,限制了有效的指令级并行开发。

  • 浮点除法延迟高:浮点除法为非流水线化操作,在浮点除法密集的代码中可能成为性能瓶颈。

性能瓶颈的定量分析

为了更深入地理解21264的性能特征,可以对各种性能限制因素进行定量分析。这种分析方法(称为性能因素分解)将理想IPC与实际IPC之间的差距分解为各个瓶颈的贡献。

理想IPC。如果没有任何瓶颈(无限容量的Cache、完美的分支预测、无限的发射队列),21264的理论最大IPC为4.0(4-wide发射)。但即使在理想条件下,程序中的数据依赖也会限制实际IPC——这称为ILP上限(Instruction-Level Parallelism ceiling)。对于典型的整数程序,ILP上限约为2.5–3.5。

各瓶颈的IPC损失估算。

  • 分支预测失败:假设SPECint95的平均误预测率为5%,分支频率约20%(每5条指令一条分支),平均恢复惩罚为8个周期。则由分支预测失败导致的IPC损失约为:
ΔIPCbranch=0.05×0.20×4×81+0.05×0.20×80.30\Delta\text{IPC}_{\text{branch}} = \frac{0.05 \times 0.20 \times 4 \times 8}{1 + 0.05 \times 0.20 \times 8} \approx 0.30
  • L1 D-Cache缺失:假设D-Cache缺失率为5%,Load指令频率约25%,L2延迟为15个周期。则:
ΔIPCdcache=0.05×0.25×4×151+0.05×0.25×150.59\Delta\text{IPC}_{\text{dcache}} = \frac{0.05 \times 0.25 \times 4 \times 15}{1 + 0.05 \times 0.25 \times 15} \approx 0.59
  • L1 I-Cache缺失:假设I-Cache缺失率为1%,缺失惩罚为15个周期:
ΔIPCicache0.01×15×4=0.60(简化估算)\Delta\text{IPC}_{\text{icache}} \approx 0.01 \times 15 \times 4 = 0.60 \text{(简化估算)}

(但64 KB I-Cache的缺失率通常很低,实际影响小于此估算。)

  • 跨Cluster延迟:约25%的指令对存在跨Cluster依赖,额外1周期延迟。等效IPC损失约0.15–0.25。

  • 发射队列满暂停:当发射队列(20条目)持续满员时,前端暂停。在高IPC代码段中,发射队列满事件频率约5%–10%,对应IPC损失约0.10–0.20。

综合以上因素,21264在SPECint95上的典型IPC约为3.00.300.400.200.102.03.0 - 0.30 - 0.40 - 0.20 - 0.10 \approx 2.0,与实际测量值吻合。

性能分析 8 — 21264各版本性能对比

Alpha 21264家族各版本的性能演进:

版本代号工艺频率SPECint2000SPECfp2000
21264EV60.35 μm466–600 MHz450–600500–700
21264AEV670.28 μm667–750 MHz600–750700–900
21264BEV68CB0.18 μm833 MHz750850
21264CEV68DC0.18 μm1–1.25 GHz800–950900–1100

可以看到,通过工艺缩放实现的频率提升几乎线性地转化为性能提升。这说明21264的微架构设计在频率提升后不存在显著的新瓶颈——IPC基本保持不变,性能与频率成正比。

多种投机机制的协同与冲突

21264中多种投机机制同时工作,它们之间存在复杂的交互关系。理解这些交互对于全面把握21264的行为至关重要。

投机机制的层次。21264中存在以下几层投机:

  1. 第一层——Line/Way预测:预测下一个取指行和Way。错误代价最低(1周期)。

  2. 第二层——分支方向预测:预测分支指令的方向。错误代价中等(7–10周期)。

  3. 第三层——存储器消歧投机:假设Load与之前的Store无冲突。错误代价较高(类似分支失败)。

  4. 第四层——Load命中/缺失预测:预测Load是否命中D-Cache。错误代价中等(重放若干指令)。

投机层次的嵌套。这些投机层次是嵌套的——后层的投机建立在前层投机正确的前提上。例如:

  • 存储器消歧投机(第三层)只对分支预测正确路径上的指令有意义。如果分支预测错误(第二层失败),错误路径上的存储器消歧结果也是无效的——但SWM可能已经错误地"学习"了来自错误路径的冲突信息。

  • Load命中/缺失预测(第四层)的投机唤醒只对Cache访问实际发生的Load有意义。如果该Load位于错误的分支路径上(第二层失败),投机唤醒本身就是浪费的。

级联恢复。当高层投机失败时,可能需要"级联"地撤销低层投机的效果。例如:

  • 分支预测失败时,错误路径上所有的投机唤醒(Load命中预测的结果)都需要被撤销。

  • 存储器顺序违规时,违规Load之后的所有指令(包括它们的投机唤醒结果)都需要被撤销。

21264通过统一的ROB顺序提交和分支掩码机制来管理这种级联恢复——任何高层投机失败都会导致错误路径上所有低层投机的结果被丢弃,保证了处理器状态的正确性。

误训练问题。一个微妙的问题是:错误路径上的执行结果可能"污染"训练表格。例如,错误路径上的Load可能缺失D-Cache,导致Load命中/缺失预测器错误地将该Load标记为"可能缺失"。当分支预测失败被检测后,该Load的预测器条目已经被错误更新。21264对这种误训练通常不进行修正——频率足够低的误训练不会显著影响预测精度,因为正确路径上的训练数据会逐渐覆盖错误的训练。

同样,Store Wait Map也可能从错误路径上的存储器消歧结果中"学习"到错误的冲突信息。但由于SWM有遗忘机制,错误的冲突标记会在后续的正确执行中被逐渐清除。

设计提示

21264对错误路径训练采取的"不修正"策略是一个务实的工程选择。完美的解决方案是在投机失败时撤销所有预测表格的更新——但这需要为每个表格维护检查点或回滚日志,存储和控制开销巨大。21264的设计者认识到,预测表格(如SWM、Load命中/缺失预测器)使用饱和计数器,具有天然的"惯性"——少量错误的训练事件会被大量正确的训练事件淹没,对预测精度的影响可以忽略。这种"近似正确"的训练策略在现代处理器中仍然普遍使用——完美的推测状态管理在工程成本上往往不划算,适度的不精确是一种值得追求的设计简化。

投机深度的限制。21264同时支持的未解析分支数量(由分支掩码位数决定)限制了投机深度。当所有掩码位都被占用时,前端暂停取指,等待最老的分支被解析后释放掩码位。这一限制在深度嵌套的条件分支代码中可能成为性能瓶颈。

投机深度限制与分支预测精度之间存在有趣的权衡:如果分支预测精度很高(>>95%),增加投机深度的收益很大——处理器可以在更多未解析分支的"阴影"下执行更多投机指令,提高有效IPC。但如果预测精度较低,深度投机反而有害——错误路径上执行的指令越多,分支预测失败时浪费的执行带宽和恢复代价越大。21264的Tournament预测器提供了足够高的精度(94%–96%),使得中等深度的投机(4–8个未解析分支)是有益的。

芯片物理设计与功耗

21264的物理设计(芯片布局、时钟分配、功耗管理)是实现其高频目标的关键因素。理解这些物理层面的设计决策有助于全面把握21264的工程权衡。

芯片布局

21264采用0.35 μm六层铝金属CMOS工艺制造,芯片面积约为16.5mm×18mm=297mm216.5\,\text{mm} \times 18\,\text{mm} = 297\,\text{mm}^2。芯片的布局设计遵循"模块化"原则,各功能单元占据芯片的不同区域:

  • I-Cache(64 KB):占据芯片的上部或一侧,是面积最大的单一结构之一。SRAM阵列以规则的二维结构排布,数据阵列和Tag阵列分别占据不同的子区域。

  • D-Cache(64 KB):与I-Cache对称地占据芯片的另一侧。

  • Ibox(取指单元):位于I-Cache附近,包括Line Predictor、Tournament分支预测器、解码逻辑和重命名映射表。

  • Ebox(整数执行单元):位于芯片中部。两个整数Cluster对称布局,跨Cluster总线是连接两个Cluster的关键互连。

  • Fbox(浮点执行单元):位于芯片的另一区域,包含FADD和FMUL功能单元及浮点寄存器文件。

  • Mbox(存储器单元):靠近D-Cache,包含Store Buffer、TLB和存储器消歧逻辑。

  • Cbox(外部接口):位于芯片边缘,靠近I/O引脚,包含MAF、Victim Buffer和总线接口逻辑。

布局对时序的影响。芯片布局直接影响信号传输延迟。在0.35 μm工艺下,跨越整个芯片(约18 mm)的信号需要2–3个时钟周期才能到达。这就是为什么21264将相互频繁通信的结构放置在相邻位置——例如I-Cache和Ibox、D-Cache和Mbox、以及两个Cluster的寄存器文件必须尽量靠近跨Cluster总线。

Cluster化设计本身也是布局友好的:两个Cluster在物理上可以被放置在芯片的对称位置,各自的功能单元和寄存器文件紧密排列,Cluster内的信号路径短而快。跨Cluster总线是两个Cluster之间唯一的数据通道,布线需求明确,易于在布局中优化。

时钟分配网络

在500 MHz以上的频率下,时钟分配(clock distribution)是芯片设计的关键挑战之一。21264使用了一个全局时钟树(global clock tree),将时钟信号从芯片中央的PLL(Phase-Locked Loop)分配到所有的流水线寄存器。

时钟偏斜控制。时钟偏斜(clock skew)是指时钟信号到达芯片不同位置的时间差异。21264的设计目标是将全局时钟偏斜控制在约200 ps以内(约为时钟周期的10%)。为此,时钟树采用了H-tree拓扑——时钟信号从中心点向四个方向等长传播,在每个分支点再次等分,保证到所有叶节点的路径长度相等。

条件时钟门控。为了降低时钟网络的动态功耗,21264在某些模块中使用了时钟门控(clock gating)技术。时钟门控通过在模块不活跃时(如发射队列中某个条目为空、浮点单元在整数密集代码中空闲等)关闭该模块的时钟信号,消除不必要的时钟切换功耗。时钟门控是21264功耗管理的主要手段之一。

时钟门控的粒度。21264的时钟门控在不同模块中使用了不同的粒度:

  • 功能单元级:当浮点单元没有待执行的指令时,整个Fbox的时钟可以被门控关闭。这在整数密集的代码中可以节省10%–15%的总功耗。

  • 发射队列条目级:发射队列中的空条目不需要参与唤醒比较,可以门控这些条目的比较器时钟。

  • Cache体级:I-Cache中未被访问的Way(通过Way预测确定)不需要激活数据SRAM的感应放大器,实现了细粒度的功耗控制。

时钟门控的使用在21264中是保守的——只在确定不影响正确性的场景下才应用。后来的处理器(如Intel的Pentium M及后续移动处理器)将时钟门控发展为更系统化的功耗管理技术。

功耗分析

21264在0.35 μm工艺、2.2 V供电电压、500 MHz频率下的典型功耗约为72 W。这一功耗水平在当时是很高的——同时代的Pentium II在相近频率下功耗约为30–40 W。21264的高功耗主要来自以下因素:

(1)大容量SRAM。128 KB的L1 Cache(I-Cache + D-Cache)贡献了总功耗的约30%–40%。每次Cache访问需要驱动大量的位线和字线,SRAM阵列的动态功耗与访问频率和阵列大小成正比。Line/Way预测通过仅读取一个Way来降低I-Cache的功耗,但D-Cache的双倍频率操作增加了D-Cache的功耗。

(2)宽发射和大ROB。4-wide的取指/解码/发射路径、80条目的ROB、20+15条目的发射队列,所有这些结构每周期都在进行大量的比较和数据搬移操作。发射队列的CAM唤醒逻辑(160个比较器每周期同时工作)是功耗热点之一。

(3)双Cluster的寄存器文件。两个80×\times64-bit的物理寄存器文件副本意味着存储容量翻倍。每次写入操作需要同时更新本地和远程两个副本,进一步增加了功耗。

(4)高频时钟分配。时钟网络本身消耗了总功耗的约25%–30%——时钟信号必须到达芯片上所有的流水线寄存器,高频率意味着高切换功耗。

性能分析 9 — 21264的功耗效率分析

尽管21264的绝对功耗较高,但从性能-功耗比(performance-per-watt)的角度来看,它在当时是合理的。在SPECint95基准测试中:

  • 21264(500 MHz, 72 W):SPECint95 = 30.3,效率 = 30.3/72 = 0.42 SPECint/W

  • Pentium II(300 MHz, 35 W):SPECint95 \approx 12,效率 \approx 12/35 = 0.34 SPECint/W

  • MIPS R10000(200 MHz, 30 W):SPECint95 \approx 8,效率 \approx 8/30 = 0.27 SPECint/W

21264在绝对性能和性能-功耗比两个指标上都领先于同时代的竞争者。但72 W的功耗对散热系统提出了严格要求——21264通常需要配备大型散热片和主动风冷,在某些服务器配置中甚至需要液冷。

功耗与工艺缩放。后续版本的21264在更先进的工艺下实现了更好的功耗控制。21264A(0.28 μm)在600 MHz下功耗约为60 W;21264C(0.18 μm,1.5 V供电)在1 GHz下功耗约为75 W。工艺缩放带来的电压降低和寄生电容减少使得频率提升40%的同时功耗仅增加了少量。

热设计与封装。72 W的功耗集中在约300mm2300\,\text{mm}^2的芯片上,功率密度约为24W/cm224\,\text{W/cm}^2。这一功率密度要求高效的散热解决方案。21264使用陶瓷柱栅阵列(Ceramic Pin Grid Array, CPGA)封装或LGA(Land Grid Array)封装。陶瓷封装比塑料封装有更好的热传导性,但成本更高。

芯片内部的功率密度分布并不均匀——执行单元区域(整数Cluster和浮点单元)的开关活动最为频繁,功率密度最高,可能形成热点(hotspot)。21264的设计者通过将高功耗单元分散在芯片的不同区域(例如两个Cluster分别位于芯片中部的两侧)来避免热点的集中。

设计验证

21264的设计验证是整个项目中工作量最大的部分之一。1520万个晶体管的复杂设计需要极其严格的验证流程来确保功能正确性和时序收敛。

验证方法。21264的验证主要依赖以下手段:

  • 形式化验证:对关键控制逻辑(如发射队列仲裁、ROB管理、分支掩码等)使用形式化方法进行数学证明,确保在所有可能的输入条件下逻辑正确。

  • 随机测试生成:使用随机指令生成器产生数百万条随机Alpha指令序列,在RTL仿真器上运行并与参考模型(ISA级仿真器)的结果比较。随机测试特别有效地发现了一些难以通过手工测试覆盖的边界情况(corner case)。

  • 定向测试:针对已知的复杂交互场景(如分支预测失败与Cache缺失同时发生、投机唤醒错误与Store Buffer转发重叠等)编写专门的测试用例。

  • 硬件原型验证:在流片前使用FPGA原型在较低频率下运行完整的操作系统(如Tru64 UNIX),验证系统级的功能正确性。

首次流片成功。21264在首次流片(first silicon)时即功能正确,无需重新投片(respin)——这在1990年代的复杂处理器设计中是非常罕见的成就。这归功于DEC严格的设计方法学和全面的验证流程。Richard Kessler在21264设计回顾中强调,充分的验证时间和方法学纪律是21264项目成功的关键因素。

设计团队与项目管理

21264的设计始于1994年,由DEC位于马萨诸塞州哈德逊(Hudson)的微处理器工程团队完成。整个设计团队约100–150人,核心设计者包括:

  • Richard Kessler:21264项目的首席架构师(Lead Architect)。Kessler在21264设计完成后发表了详细的设计回顾论文("The Alpha 21264 Microprocessor", IEEE Micro, 1999年),这篇论文成为处理器设计领域的经典参考文献。

  • Dirk Meyer:项目的主要管理者之一,后来成为AMD的CEO。

  • Joel Emer:参与了分支预测器和流水线设计。Emer后来加入Intel并在学术界做出重要贡献,他关于处理器性能分析方法论的工作广受引用。

设计周期。从设计开始(1994年)到首次流片(1996年末)约2.5年,从流片到产品发布(1998年)约1.5年(用于测试、验证和频率优化)。整个项目周期约4年,在当时的处理器设计中属于正常范围。设计团队使用Verilog进行RTL描述,使用DEC内部的设计工具链进行综合、布局和时序分析。

设计方法学的特点。21264的设计方法学在当时有几个突出特点:

  1. 微架构探索先行:在RTL编码之前,设计团队花费了约一年时间进行微架构探索——使用性能仿真器评估不同的设计方案(如不同的Cache容量、ROB大小、分支预测器配置等),在方案空间中搜索最优的参数配置。这种"仿真驱动的设计"方法避免了在RTL阶段才发现架构选择的错误。

  2. 时序驱动的微架构决策:许多微架构决策(如Cluster化、Way预测、压缩式发射队列)不是仅基于功能考虑,而是由时序约束驱动的。设计团队在微架构阶段就进行了关键路径分析,确保提出的方案在目标频率下可实现。

  3. 全芯片RTL仿真:整个芯片(1520万晶体管)在流片前经过了完整的RTL仿真,运行了数亿条指令的测试向量。这在当时的计算能力下是一个巨大的工程挑战。

21264的历史地位与设计遗产

21264的技术贡献

Alpha 21264是处理器设计史上的一座里程碑。它在有限的晶体管预算下,通过精巧的微架构设计实现了当时最高的性能。21264引入的多项技术成为后续处理器设计的标准范式:

(1)Tournament分支预测器。21264是第一个在商用处理器中成功应用混合(hybrid)分支预测器的设计。它证明了通过动态选择器在多个预测策略之间切换,可以获得优于任何单一预测器的精度。这一思想直接影响了后续所有混合预测器的设计,包括McFarling的后续工作、bi-mode预测器、以及最终催生了当今主流的TAGE预测器家族。

(2)Cluster化执行。21264的双Cluster整数执行结构开创了处理器Cluster化设计的先河。它展示了如何通过物理寄存器文件的复制和跨Cluster通信总线,在可接受的面积和延迟约束下实现宽发射。这一技术路径在后来的AMD K7/K8、Intel Atom、以及多种嵌入式处理器中得到了应用。

(3)Line/Way预测。21264的Line Predictor是Way预测技术的先驱。它展示了如何通过预测来消除set-associative Cache中的Way选择延迟,将多路关联Cache的访问延迟降低到接近直接映射Cache的水平。这一技术后来在AMD Zen系列的L1 D-Cache Way预测和ARM Cortex-A系列的多种核心中得到了广泛应用。

(4)投机存储器消歧。Store Wait Map是最早的基于历史的存储器消歧预测机制之一。它建立了"默认投机、历史学习"的范式——对于不确定的存储器依赖关系,先假设最乐观的情况,在检测到错误后学习并调整后续行为。这一范式后来被所有主流处理器采用,包括Intel的Memory Disambiguator和AMD的Memory Dependence Predictor。

(5)Load命中/缺失预测。21264是最早将Cache命中/缺失预测应用于投机唤醒控制的处理器之一。这一机制在后来的处理器中演化为更复杂的形式,如Intel的数据预取器中的命中/缺失分类、以及现代处理器中基于Load延迟预测的调度优化。

Alpha团队的人才扩散

1998年Compaq收购DEC后,Alpha团队的多位核心设计者加入了其他处理器公司,将21264的设计理念带到了新的平台:

  • Joel Emer等人加入Intel,参与了Pentium 4和后续微架构的设计。Emer后来在Intel的微架构研究中做出了重要贡献,包括对性能分析方法论的推进。

  • 多位Alpha设计者加入AMD,参与了K7/K8(Athlon/Opteron)的设计。K8的一些微架构特性——如独立的整数/浮点调度器、Tournament风格的混合预测器、分离的整数Cluster——明显受到了21264的影响。AMD K7被普遍认为是Alpha设计哲学在x86世界中的体现。

  • Dirk Meyer曾是21264项目的主要领导者之一,后来成为AMD的CEO(2008–2011年),在此期间推动了AMD处理器架构的重要战略决策。

  • 部分设计者加入了Apple、Broadcom、PA Semi等公司的处理器团队。PA Semi后来被Apple收购,其团队成为Apple自研芯片(A系列、M系列处理器)的技术核心之一。从这个角度看,21264的设计基因通过多次人才流动,最终融入了当今最先进的消费级处理器中。

从这个意义上说,21264的影响远远超出了Alpha架构本身——它的设计思想通过这些工程师扩散到了整个处理器工业,深刻影响了后续二十年的处理器微架构发展。

21264的后继设计

21264之后,Alpha团队设计了两款后继处理器,但由于DEC的收购和Alpha架构的最终终止,这些设计未能完全实现其潜力:

Alpha 21264A/B/C(EV67/EV68)。这些是21264的工艺缩减版本(die shrink),分别采用0.28 μm和0.18 μm工艺,微架构基本不变,但时钟频率从466 MHz逐步提升到1.25 GHz。EV68增加了对Alpha ISA扩展指令(如字节级操作BWX、多媒体MVI等)的支持。

Alpha 21364(EV7)。21364是21264的集成化版本,最大的变化是集成了片上L2 Cache(1.75 MB)和存储器控制器。微架构核心与21264基本相同(包括相同的Tournament预测器、双Cluster整数执行、相同容量的L1 Cache等),但通过集成L2 Cache将L2访问延迟从12–20个周期降低到约7个周期,显著提升了存储器敏感型工作负载的性能。21364采用0.18 μm工艺,集成了1.52亿个晶体管(约为21264的10倍),频率达到1.15 GHz。

Alpha 21464(EV8)。EV8是Alpha团队最雄心勃勃的设计——一款8-wide发射的超标量处理器,计划采用4个整数Cluster(每个Cluster 2个ALU)、512个条目的ROB、同时多线程(SMT)等先进特性。

EV8的关键设计目标和创新包括:

  • 8-wide发射:取指和发射宽度从21264的4扩展到8,理论IPC上限翻倍。为支持这一宽度,EV8计划使用4个整数Cluster(21264的2个翻倍),每个Cluster仍然保持2个ALU。

  • 512条目ROB:大幅增加指令窗口以开发更多的指令级并行性。512条目是21264的6.4倍,允许处理器在长延迟缺失期间继续向前推进更远。

  • 同时多线程(SMT):EV8计划支持4个硬件线程的同时执行。SMT允许在一个线程因Cache缺失或依赖暂停时,另一个线程利用空闲的执行带宽,提高功能单元利用率。

  • 值预测(Value Prediction):EV8计划引入值预测机制——在数据实际计算或加载完成之前,预测其值。如果预测正确,依赖链上的后续指令可以使用预测值提前执行;如果预测错误,类似于分支预测失败需要回滚。值预测是突破数据依赖瓶颈的激进技术。

  • 4 MB片上L2 Cache:在0.10 μm工艺下,晶体管预算充裕,EV8计划集成大容量的片上L2 Cache。

然而,由于Compaq被HP收购(2002年)以及随后Alpha架构的终止,EV8项目在设计阶段被取消,从未流片。尽管如此,EV8的设计研究产生了多篇有影响力的学术论文,其中关于同时多线程和值预测的研究对后续的处理器设计理念产生了重要影响。

案例研究 7 — 21264对AMD K7/K8的影响

AMD K7(Athlon, 1999年)和K8(Opteron/Athlon 64, 2003年)是AMD在x86领域取得竞争力的关键处理器。这两款处理器的微架构明显受到了21264设计哲学的影响——这并非巧合,因为AMD在1990年代末招募了多名前Alpha设计工程师。

K7中的21264影子:

  • 3-wide解码、9-wide发射:K7将x86指令解码为类RISC的内部操作(macro-ops/micro-ops),然后使用宽发射的乱序后端执行。这种"前端CISC解码+后端RISC执行"的架构思路虽然不是Alpha独创的(Intel P6先行),但K7的后端宽度和乱序引擎规模更接近21264而非P6。

  • 分离的整数/浮点调度器:K7使用独立的整数发射队列和浮点发射队列,与21264的设计完全相同。相比之下,同时代的Intel P6使用统一的调度器。

  • 大容量L1 Cache:K7的L1 I-Cache和D-Cache各64 KB,与21264完全相同。这一选择在当时的x86处理器中是独特的(Intel P6只有16 KB I-Cache + 16 KB D-Cache)。

  • 混合分支预测器:K7使用的分支预测器包含全局和局部两个组件,通过选择器进行动态选择——这是Tournament预测器思想的直接应用。

K8中的进一步继承:

  • 集成存储器控制器:K8是第一款集成片上存储器控制器的x86处理器。这一设计选择与Alpha 21364(EV7)——21264的集成化后继——的思路完全一致。K8的设计者很可能借鉴了EV7的集成经验。

  • 点对点互连:K8使用HyperTransport点对点互连替代传统的共享前端总线,这与21264的EV6点对点总线协议在概念上一脉相承。

从K7/K8的巨大商业成功可以看到,21264的设计理念经过x86适配后证明了其普遍性——这些技术不仅在RISC架构上有效,在CISC架构上同样能够发挥威力。

设计哲学的总结

21264证明了一个重要的设计哲学:在有限资源下,通过精准的投机执行和精巧的微架构权衡,可以实现超越资源优势对手的性能。这一哲学至今仍然是处理器设计的核心指导思想。

设计权衡 1 — 21264的设计权衡总结

回顾21264的整体设计,可以提炼出以下核心设计权衡:

设计选择收益代价
Line/Way预测单周期I-Cache访问预测错误时1周期惩罚
Tournament预测器高精度方向预测约3.6 KB存储开销
双Cluster整数执行降低寄存器文件端口需求跨Cluster 1周期额外延迟
投机存储器消歧降低Load等待延迟违规时需要重放
Load命中/缺失预测最短的Load-use延迟预测错误时需要重发
放弃片上L2 Cache更大的L1 Cache和乱序引擎依赖片外L2,延迟增加
80条目ROB大指令窗口面积和功耗增加
WALK恢复(非检查点)节省检查点存储面积恢复延迟可变
双倍频率D-Cache等效双端口,面积小每次访问的时间预算减半
全相联TLB无冲突缺失CAM面积较大

这些权衡的共同主题是以投机换延迟:21264在多个环节中通过投机执行(Way预测、分支预测、存储器消歧、Load命中预测)来缩短关键路径延迟,接受少量投机失败的惩罚。这种设计策略在投机准确率足够高时(通常>>90%),总体性能收益远大于偶尔投机失败的代价。

21264的另一个核心设计原则是以面积换时序:通过复制关键结构(如双Cluster的PRF副本、映射表副本),降低每个结构的端口需求和规模,使关键路径延迟满足目标频率。这一原则在后续的处理器设计中被反复应用——现代处理器中的多体(banked)寄存器文件、分区发射队列等技术,都是"以面积换时序"思想的延伸。

第三个原则是基于历史的自适应投机:21264在多个组件中使用了相同的设计模式——维护一个以PC索引的历史表,根据过去的行为预测未来是否应该投机执行(Tournament预测器、Store Wait Map、Load命中/缺失预测器)。这种"历史驱动"的投机控制范式在现代处理器中无处不在,从分支预测到预取、从功耗管理到线程调度,都采用了类似的自适应机制。

投机预测器的统一设计模式。回顾21264中的各种预测器,可以发现它们共享一个统一的设计模式:

  1. 索引机制:使用程序计数器(PC)或其派生值作为索引。PC是指令身份的天然标识符——同一条指令在多次执行中使用相同的PC,这使得基于PC的预测表可以捕获特定指令的历史行为。

  2. 状态表示:使用饱和计数器(2-bit或3-bit)记录历史。饱和计数器具有天然的"惯性"——偶尔的异常事件不会立即改变预测方向,提供了对噪声的鲁棒性。

  3. 更新规则:正确时向一个方向递增,错误时向另一个方向递减。这种对称的更新规则使得预测器能够在程序行为变化时自适应地调整。

  4. 默认假设:在没有历史信息时(如冷启动),选择一个合理的默认值。对于分支预测器,默认为not-taken(统计上更常见);对于SWM,默认为"无冲突"(大多数Load与之前的Store无冲突);对于Load命中预测器,默认为"命中"(大多数Load命中D-Cache)。

21264的晶体管利用效率。将21264的1520万晶体管与同时代处理器进行对比:

  • Intel Pentium II(1997年):750万晶体管,约一半用于L1 Cache(16 KB+16 KB),另一半用于x86解码和微操作缓冲。

  • HP PA-8500(1999年):1.4亿晶体管,但其中绝大部分(约1.3亿)用于片上1.5 MB L1 Cache,核心逻辑的晶体管数量与21264相当。

  • 21264:1520万晶体管中,约50%用于128 KB L1 Cache,约50%用于乱序执行引擎。

21264在核心逻辑(去除Cache后)的晶体管利用效率极高——用约760万晶体管实现了4-wide乱序执行引擎、80条目ROB、双Cluster PRF、Tournament预测器、Store Wait Map等全部微架构创新。这一效率反映了DEC设计团队对晶体管预算的精细管理和对微架构创新的深刻理解。

设计提示

21264的设计给后来的处理器工程师留下了最重要的启示或许是:微架构创新比晶体管数量更重要。21264用1520万个晶体管击败了HP PA-8500的1.4亿个晶体管,说明聪明的微架构设计可以弥补一个数量级的晶体管劣势。当然,随着工艺进步使晶体管变得廉价,"暴力"方案(更大的Cache、更多的执行单元)也有其价值。但21264的经验提醒我们,在任何给定的晶体管预算下,微架构设计的质量是决定性能的首要因素。

代码清单 lst:ch40-cluster-bypass给出了21264双Cluster间旁路仲裁的简化SystemVerilog实现。核心设计点在于:Cluster内旁路为0周期延迟(同拍可用),Cluster间旁路需要额外1周期(通过寄存器打拍传递)。仲裁逻辑需要在本地旁路、跨Cluster旁路和寄存器文件读取之间选择正确的数据源,优先级为:本地旁路 >> 跨Cluster旁路 >> 寄存器文件。

verilog
module cluster_bypass_arbiter #(
    parameter DATA_W  = 64,    // 数据宽度
    parameter PREG_W  = 7,     // 物理寄存器索引宽度 (80 regs)
    parameter N_LOCAL = 2,     // 每Cluster本地FU数
    parameter N_REMOTE= 2      // 对端Cluster FU数
)(
    input  logic                 clk, rst_n,
    // 消费者指令的源操作数标签
    input  logic [PREG_W-1:0]    src_tag,
    // 本地Cluster旁路(同拍可用,0周期延迟)
    input  logic [N_LOCAL-1:0]           local_valid,
    input  logic [PREG_W-1:0]            local_tag   [N_LOCAL],
    input  logic [DATA_W-1:0]            local_data  [N_LOCAL],
    // 跨Cluster旁路(上一拍结果,1周期延迟)
    input  logic [N_REMOTE-1:0]          remote_valid,
    input  logic [PREG_W-1:0]            remote_tag  [N_REMOTE],
    input  logic [DATA_W-1:0]            remote_data [N_REMOTE],
    // 寄存器文件读取(最低优先级)
    input  logic [DATA_W-1:0]            prf_data,
    // 仲裁输出
    output logic [DATA_W-1:0]            result_data,
    output logic                         result_valid
);

    // --- 本地旁路匹配(最高优先级)---
    logic              local_hit;
    logic [DATA_W-1:0] local_fwd;

    always_comb begin
        local_hit = 1'b0;
        local_fwd = '0;
        for (int i = N_LOCAL-1; i >= 0; i--) begin
            if (local_valid[i] && local_tag[i] == src_tag) begin
                local_hit = 1'b1;
                local_fwd = local_data[i];
            end
        end
    end

    // --- 跨Cluster旁路匹配(次高优先级)---
    // 注意:remote_data已经是上一拍寄存打拍后的值
    logic              remote_hit;
    logic [DATA_W-1:0] remote_fwd;

    always_comb begin
        remote_hit = 1'b0;
        remote_fwd = '0;
        for (int i = N_REMOTE-1; i >= 0; i--) begin
            if (remote_valid[i] && remote_tag[i] == src_tag) begin
                remote_hit = 1'b1;
                remote_fwd = remote_data[i];
            end
        end
    end

    // --- 优先级仲裁: local > remote > PRF ---
    always_comb begin
        result_valid = 1'b1;
        if (local_hit) begin
            result_data = local_fwd;       // 0周期: 本地旁路
        end else if (remote_hit) begin
            result_data = remote_fwd;      // 1周期: 跨Cluster旁路
        end else begin
            result_data = prf_data;        // 寄存器文件读取
        end
    end
endmodule

设计提示

上述实现中有两个关键的工程细节值得注意:(1)本地旁路的优先级高于跨Cluster旁路——当同一物理寄存器在本地和远端Cluster同时有旁路数据时(例如一条指令被重放后在不同Cluster执行),本地旁路的数据更新(0周期延迟 vs 1周期延迟),因此优先级更高。(2)跨Cluster旁路数据的寄存打拍在代码中并未显式体现——在21264的实际设计中,每个Cluster输出的结果会经过一级锁存器后送到对端Cluster的旁路MUX,这一级锁存器正是1周期额外延迟的来源。对端Cluster的调度器在发射指令时,必须考虑这1周期延迟——如果依赖的生产者在对端Cluster执行,消费者的发射必须延迟1周期。

Alpha 21264在1998年展示了投机与并行的极致工程实践。接下来,第 41.0 章将考察另一个同样影响深远的微架构系列——Intel Core。从2011年Sandy Bridge引入的μ\muop Cache(消除x86解码瓶颈,参见第 23.0 章),到Golden Cove的512-entry ROB和6-wide后端,再到Alder Lake的混合架构(P-core/E-core),Intel Core系列在十余年间将21264开创的乱序执行范式推向了新的极限。我们将看到,21264的许多创新——Cluster化、大容量ROB、投机Load执行——在Intel处理器中以更大规模和更精细的形式延续至今。

正文与图片:CC BY-NC-SA 4.0 · 本仓库少量站点配置代码:MIT