Skip to content

超标量处理器的流水线结构

1999年,Intel推出了代号Willamette的初代Pentium 4处理器,采用激进的20级流水线设计,目标直指高频。Pentium 4家族最终将频率推到了3.8 GHz(Prescott,31级流水线),但IPC却比上一代Pentium III显著倒退。五年后的2006年,Intel推出了Core 2 Duo——仅14级流水线、频率"仅"2.4 GHz,却在几乎所有工作负载上全面超越Pentium 4。深流水线的陷阱是处理器设计史上最昂贵的教训之一:频率和IPC不是独立的旋钮,流水线深度是它们之间的耦合变量

设计提示

统一视角。流水线的深度和宽度是投机并行的第一个重大权衡。更深的流水线通过缩短每级的逻辑深度来提高频率,但也增加了分支预测失败时需要清空的级数——即投机的代价。更宽的流水线通过每周期发射更多指令来挖掘并行性,但每增加一路发射,重命名逻辑、旁路网络和发射队列的复杂度都呈超线性增长——即并行的代价第 1.0 章引入的性能公式T=N×CPI×TcycleT = N \times \text{CPI} \times T_{\text{cycle}}中,流水线深度主要影响TcycleT_{\text{cycle}}和CPI中的分支惩罚分量,流水线宽度主要影响CPI的基础分量。本章将用定量模型揭示这些权衡的数学本质。

现代高性能处理器的流水线结构是超标量设计的骨架。第第 1.0 章章从宏观角度概述了超标量处理器的基本概念和性能公式,本章将深入到流水线的每一个阶段,剖析其硬件实现细节和设计权衡。流水线宽度与深度的选择是处理器微架构设计中最早、最关键的决策——它们决定了后端所有部件(发射队列、物理寄存器文件、ROB、旁路网络等)的规模和复杂度,也直接影响了处理器的IPC、频率和功耗。

一条指令从被取出到最终退休,需要经历十余个流水阶段。每个阶段都有独特的硬件结构和设计挑战。本章首先概述每个阶段的功能和典型延迟,然后深入分析流水线宽度和深度的选择背后的数学模型与工程权衡,最后讨论预测技术在超标量处理器中的核心角色。

流水线各阶段概述

一个典型的现代乱序超标量处理器的流水线包含15\sim22个阶段,按功能可以分为前端(Front-End)、中端(Mid-End)和后端(Back-End)三大部分。前端负责取指令和解码,中端负责寄存器重命名和分发,后端负责乱序发射、执行、写回和顺序提交。图图 2.1展示了一个完整的现代OoO处理器数据通路。

现代乱序超标量处理器的完整数据通路(简化示意图)。蓝色虚线标示前端-中端-后端的分界。红色虚线表示控制流(清空/恢复信号)。
现代乱序超标量处理器的完整数据通路(简化示意图)。蓝色虚线标示前端-中端-后端的分界。红色虚线表示控制流(清空/恢复信号)。

表 2.1总结了各阶段的典型延迟(以流水线级数计)。需要注意的是,不同处理器的划分方式存在差异,此处给出的是多款现代高性能核心的近似值范围。

阶段主要操作级数说明
分支预测BTB/TAGE查询2–3多级预测器需多周期
取指I-Cache访问、PC生成2–3含I-TLB查询
预解码指令边界识别、预解码1–2RISC定长指令可省略
解码指令译码、uop生成1–3x86需额外级数
重命名RAT查询、物理寄存器分配1–2含Free List操作
分发资源检查、写入IQ/ROB1含背压(backpressure)检查
发射唤醒+选择1关键路径
读寄存器PRF读端口访问0–1可与发射合并
执行ALU/FPU/LSU运算1–6+依指令类型而定
写回PRF写端口、旁路广播1与唤醒同周期
提交ROB队头退休、状态更新1–2含异常/中断检查

现代OoO超标量处理器各流水线阶段的典型延迟

值得注意的是,表表 2.1中给出的"级数"是指该阶段的工作被划分为多少个流水段。例如,"取指2\sim3级"意味着I-Cache访问被切割为2\sim3个流水段,每段在一个时钟周期内完成部分工作。这种切割的目的是减少每一级的逻辑延迟,从而允许更高的时钟频率。但切割也引入了额外的流水线寄存器延迟(setup time + clock-to-q),以及增加了分支预测失败时需要清除的流水段数量。

下面逐一详细讨论每个阶段的硬件实现。

取指令(Fetch)

取指阶段是流水线的入口,其任务是在每个时钟周期从I-Cache中取出一组指令(称为取指块,fetch block),并将其送往下游的解码器。取指阶段的带宽直接决定了后端能获得多少指令来发掘指令级并行性(ILP)。

PC生成与分支预测的耦合

在经典五级流水线中,PC的生成逻辑很简单:如果当前指令不是分支,则PCnext=PC+4\text{PC}_{\text{next}} = \text{PC} + 4;如果是taken分支,则PC来自分支目标。但在现代超标量处理器中,情况要复杂得多。由于每个周期取出的是一个包含多条指令的取指块(而非单条指令),PC生成逻辑必须与分支预测器紧密耦合:分支预测器不仅要预测下一个PC,还要预测取指块中哪些指令是分支指令、这些分支是否跳转、以及跳转目标地址。

现代处理器中,PC生成和分支预测形成一个自驱动循环(self-driving loop):

  1. 当前PC送入BTB(Branch Target Buffer),查询该地址范围内是否存在分支指令;

  2. BTB返回分支的预测方向和目标地址;

  3. 如果预测taken,下一个PC设为目标地址;如果预测not taken或BTB未命中,PC递增一个取指块的大小(通常为32或64字节);

  4. 下一个周期使用新的PC重复上述过程。

这个循环的延迟——称为最小取指重定向延迟(minimum fetch redirect latency)——决定了taken分支之后取指的恢复速度。在大多数现代处理器中,这个循环需要1\sim2个周期。如果BTB的查询需要2个周期(例如在大容量BTB中),则每遇到一条taken分支,取指管线就会产生一个1周期的"气泡"(bubble),降低有效取指带宽。为了缓解这个问题,一些处理器采用了多级BTB设计:小容量的L1 BTB(例如64项)可以在1个周期内完成查询,大容量的L2 BTB(例如4K\sim12K项)需要2\sim3个周期。L1 BTB的预测结果在第一个周期就可以使用,L2 BTB的结果在后续周期覆盖L1的预测(如果L1未命中)。

I-Cache访问

I-Cache的访问是取指阶段的核心操作。现代高性能处理器的L1 I-Cache通常为32KB或64KB,采用8-way组相联结构,行大小(cache line size)为64字节。I-Cache的访问延迟为2\sim4个周期,包含以下步骤:

  1. 索引计算:使用PC的低位(例如对于32KB/8-way/64B行的I-Cache,使用PC[11:6]作为组索引,共32KB/(8×64B)=6432\text{KB}/(8\times64\text{B})=64组)对I-Cache SRAM阵列进行索引。与此同时,PC被送入I-TLB(Instruction TLB)进行虚拟地址到物理地址的翻译;

  2. 标签比较与路选择:将I-TLB返回的物理地址标签与该组中所有8路的标签进行并行比较,确定是否命中以及命中在哪一路。在VIPT(Virtually Indexed Physically Tagged)设计中,索引使用虚拟地址(无需等待TLB翻译),标签使用物理地址,使得索引计算和TLB翻译可以并行进行;

  3. 数据读取与对齐:从命中的路中读出64字节的Cache行,然后根据PC在行内的偏移(PC[5:0]对于64B行)提取出有效的指令字节。对于32字节的取指宽度,需要从Cache行的某个偏移开始截取32字节。

关于I-Cache的详细设计——包括替换策略、预取机制、与L2 Cache的交互等——将在第第 5.0 章\sim第 6.0 章章中详细讨论。

取指带宽与前端瓶颈

性能分析 1 — 取指带宽与IPC上限

取指带宽是超标量处理器性能的第一道瓶颈。假设处理器每周期取出32字节的数据,RISC-V指令为4字节(不考虑C扩展),则每周期最多取出8条指令。对于一个6-wide解码的处理器,32字节的取指带宽通常足够。但如果解码宽度为8-wide或10-wide,则需要64字节的取指带宽来保证前端不会成为瓶颈。Apple Firestorm/Everest和AMD Zen 5都采用了每周期取指64字节的设计。

然而,实际的取指带宽往往受到以下因素的限制:

  • 分支切断(Branch Cutoff):如果取指块中间存在一条taken分支,则该分支之后的指令是无效的,取指块的有效宽度被"切断"。统计表明,在典型的整数程序中,平均每6\sim7条指令就有一条分支,其中约50%是taken的,因此取指块的平均有效宽度只有理论最大值的60%\sim80%。

  • Cache行边界:如果PC跨越了Cache行的边界,则需要访问两个Cache行,这可能需要额外的周期(除非I-Cache支持跨行读取,但这会显著增加硬件复杂度)。

  • I-Cache缺失:L1 I-Cache缺失需要访问L2 Cache(延迟10\sim15周期)甚至L3 Cache(延迟30\sim50周期),导致取指长时间停顿。

  • I-TLB缺失:I-TLB缺失需要进行页表遍历(page table walk),延迟可达数百周期。

对于x86处理器,取指阶段还包含一个重要的优化——微操作缓存(Micro-op Cache,也称为Decoded Stream Buffer,DSB)。当指令已经被解码为微操作并缓存在微操作缓存中时,可以直接从微操作缓存中读取微操作,绕过I-Cache访问和解码阶段。Intel从Sandy Bridge开始引入了微操作缓存(容量约1.5K uops),到Golden Cove增大到约4K uops。AMD Zen系列也有类似的Op Cache,Zen 4的容量为6.75K uops。微操作缓存对于热点循环(hot loop)特别有效,因为这些循环的指令会反复执行,可以完全从微操作缓存中供给,消除了解码延迟和解码功耗。

取指块对齐与指令队列

从I-Cache取出的取指块需要送入指令队列(Instruction Queue,也称Fetch Queue或Instruction Buffer)。指令队列起到前后端之间的缓冲作用:当后端(解码器)暂时无法消费指令时(例如遇到复杂指令需要多周期解码),指令队列可以吸收前端取出的指令,避免前端立即停顿。典型的指令队列容量为20\sim50条指令(或等价的字节数)。

在RISC指令集中(如RISC-V基础指令集),由于所有指令都是32位对齐的,取指块内的指令提取比较简单——从任意4字节对齐的偏移开始,每4字节就是一条指令的开始。但RISC-V的C扩展引入了16位的压缩指令(混合16位和32位编码),使得指令边界不再是固定对齐的,需要逐条检查指令的低2位来判断长度。类似地,ARM的AArch64模式的基本指令为32位,但其可选的SVE2扩展引入了可变长度的向量指令。x86指令长度更是从1字节到15字节不等,指令边界的确定需要串行解析前缀和操作码字节。对于这些变长指令集,需要一个预解码阶段来确定指令边界,这增加了前端的延迟。

Intel和AMD的x86处理器在I-Cache填充时(即Cache行从L2写入L1 I-Cache时)就预先进行预解码,将每条指令的长度和开始/结束标记存储在I-Cache的辅助位中。这样在取指时可以直接从辅助位中获得指令边界信息,而不需要在取指关键路径上进行指令长度计算。这种"预解码标记"(pre-decode mark)技术将指令长度解析的延迟从取指阶段转移到了I-Cache填充阶段,以增加I-Cache的存储开销为代价(每字节需要额外1\sim2位标记位)换取了更快的取指。

解码(Decode)

解码器的任务是将取指阶段获得的指令翻译为处理器内部使用的微操作(micro-operation,简称uop或μ\muop)。每条微操作通常包含以下信息:操作类型(ALU/MUL/FP/LOAD/STORE/BRANCH等)、源操作数标识(2\sim3个源寄存器编号)、目的操作数标识(1个目的寄存器编号)、立即数、以及各种控制标志(如是否是最后一条uop、是否需要异常检查等)。

RISC与CISC解码复杂度差异

RISC指令集的解码复杂度远低于CISC。以RISC-V为例,其基础指令集(RV64I)只有6种指令格式(R/I/S/B/U/J),所有指令都是32位宽,操作码位于固定位置(inst[6:0]),源寄存器和目的寄存器字段在所有格式中的位置高度一致(rd总是在inst[11:7],rs1总是在inst[19:15],rs2总是在inst[24:20])。这种规整的编码使得一个RISC-V解码器只需要几百个逻辑门就可以完成解码,并且大部分解码可以在一个流水段内完成。

以下是一个简化的RISC-V解码器核心逻辑(SystemVerilog):

verilog
module rv64i_decoder (
  input  logic [31:0] inst,
  output logic [4:0]  rd, rs1, rs2,
  output logic [63:0] imm,
  output logic [3:0]  alu_op,
  output logic        is_load, is_store, is_branch,
  output logic        rd_valid, rs1_valid, rs2_valid
);
  // 寄存器字段提取 —— 位置固定,无需解码
  assign rd  = inst[11:7];
  assign rs1 = inst[19:15];
  assign rs2 = inst[24:20];

  // 操作码解析
  logic [6:0] opcode = inst[6:0];
  logic [2:0] funct3 = inst[14:12];
  logic [6:0] funct7 = inst[31:25];

  always_comb begin
    // 默认值
    imm = '0; alu_op = 4'b0;
    is_load = 1'b0; is_store = 1'b0; is_branch = 1'b0;
    rd_valid = 1'b0; rs1_valid = 1'b0; rs2_valid = 1'b0;

    case (opcode)
      7'b0110011: begin // R-type (ADD, SUB, ...)
        alu_op = {funct7[5], funct3};
        rd_valid = 1'b1; rs1_valid = 1'b1; rs2_valid = 1'b1;
      end
      7'b0010011: begin // I-type (ADDI, ...)
        imm = {{52{inst[31]}}, inst[31:20]};
        alu_op = {1'b0, funct3};
        rd_valid = 1'b1; rs1_valid = 1'b1;
      end
      7'b0000011: begin // Load (LW, LD, ...)
        imm = {{52{inst[31]}}, inst[31:20]};
        is_load = 1'b1;
        rd_valid = 1'b1; rs1_valid = 1'b1;
      end
      7'b0100011: begin // Store (SW, SD, ...)
        imm = {{52{inst[31]}}, inst[31:25], inst[11:7]};
        is_store = 1'b1;
        rs1_valid = 1'b1; rs2_valid = 1'b1;
      end
      7'b1100011: begin // Branch (BEQ, BNE, ...)
        imm = {{51{inst[31]}}, inst[31], inst[7],
               inst[30:25], inst[11:8], 1'b0};
        is_branch = 1'b1;
        rs1_valid = 1'b1; rs2_valid = 1'b1;
      end
      default: ;
    endcase
  end
endmodule

可以看到,RISC-V的解码器核心逻辑非常简洁:寄存器字段直接从固定位置提取(3个assign语句),操作码通过一个case语句解析。将这个模块复制nn份就构成了一个nn-wide的解码器,各个实例之间完全独立,没有串行依赖。

相比之下,x86指令的解码极为复杂。x86指令由可选的前缀字节(Legacy Prefix、REX Prefix、VEX/EVEX Prefix)、操作码字节(1\sim3字节)、ModR/M字节、SIB字节、位移字段(0/1/2/4字节)和立即数字段(0/1/2/4字节)组成,总长度从1字节到15字节不等。解码器首先需要确定每条指令的长度(这本身就是一个串行过程,因为前一条指令的结束位置才是后一条指令的开始位置),然后才能解析各字段的含义并翻译为微操作。

在Intel的处理器中,解码器通常采用"1+N"结构:一个复杂解码器(Complex Decoder)可以将一条复杂的x86指令翻译为最多4条uops,而NN简单解码器(Simple Decoder)每个只能处理可以翻译为1条uop的简单指令。在Golden Cove中,解码器为"1+5"结构,即每周期最多解码6条x86指令(其中最多1条复杂指令),产生最多6+3=9条uops(1条复杂指令最多产生4条uops,5条简单指令各产生1条uop,总共最多9条;但受制于后端宽度,通常限制为每周期最多6\sim8条uops输出)。对于需要超过4条uops的复杂指令(如REP MOVSBCPUID等),需要查询微码ROM(Microcode ROM),由微码引擎逐条发射微操作,这会严重降低解码吞吐量,因为在微码模式下,其他解码器通常被暂停。

设计权衡 1 — 解码宽度的选择

解码宽度的增加并不是简单的线性扩展。对于RISC指令集,增加一个解码器的硬件开销相对较小(因为每个解码器都是简单的组合逻辑),但对于x86指令集,由于变长编码的串行依赖,增加解码器的复杂度呈超线性增长。具体而言:

  • RISC-V/AArch64解码器:每个解码器约占300\sim500个等效门(gate equivalents),6-wide和8-wide解码器的面积差异为线性增长。整个解码器阵列的面积在核心总面积中占比不到5%。

  • x86解码器:确定指令边界的逻辑是串行的——必须知道第1条指令的长度才能找到第2条指令的开始位置,以此类推。Intel和AMD通过预解码标记(pre-decode mark)技术部分缓解了这个问题。即便如此,x86解码器阵列的面积仍然是RISC解码器的5\sim10倍,功耗也高出数倍。

  • 微操作缓存的缓解作用:在热点代码中,微操作缓存可以完全绕过解码器,使得解码器的宽度不再是瓶颈。这也是为什么Intel和AMD在将解码宽度增加到6-wide甚至8-wide时,同时大幅增加了微操作缓存的容量——对于无法命中微操作缓存的冷代码,解码器宽度才是瓶颈。

因此,RISC指令集在解码宽度的扩展上有天然的优势,这也是Apple能够将解码宽度做到10-wide的重要原因之一(Apple的处理器使用AArch64指令集)。对于x86架构,微操作缓存是扩展有效解码宽度的关键技术。

指令融合

在解码阶段,现代处理器还会执行一项重要的优化——指令融合(Instruction Fusion)。指令融合将两条相邻的指令合并为一条微操作来处理,从而减少后端需要处理的微操作数量,等效地增加了流水线宽度。

常见的融合类型包括:

  • 宏融合(Macro-Fusion):将一条比较/测试指令与紧随其后的条件分支指令合并为一条"比较并分支"微操作。例如在x86中,CMP+JNE可以融合为一条uop;在RISC-V中,BEQ/BNE等指令本身就已经包含了比较和分支的功能,天然不需要宏融合。Intel Golden Cove和AMD Zen 4每周期可以融合最多1对宏融合指令。ARM Cortex-X4也支持比较+分支的宏融合。

  • 微融合(Micro-Fusion):这是x86特有的优化。将一条包含内存操作的指令中的地址计算和数据操作合并为一条uop进行跟踪(但在发射时可能会拆分为两条uop分别发射到不同的端口)。例如x86的ADD RAX, [RBX+RCX*4+8]在解码时被当作一条uop在ROB中占一个表项,但在发射时拆分为一条load uop和一条ALU uop。微融合可以有效减少ROB的占用率,等效地增大了ROB的有效容量。

  • Move消除(Move Elimination):将寄存器到寄存器的移动指令(如x86的MOV RAX, RBX或AArch64的MOV X0, X1)在重命名阶段直接通过修改RAT映射来实现,而不需要占用执行端口。Intel从Ivy Bridge开始支持Move消除(每周期最多消除4条),AMD从Zen开始支持。RISC-V的MV伪指令(实际为ADDI rd, rs, 0)在某些实现中也可以被消除。

  • 零惯用语识别(Zeroing Idiom Recognition):将清零操作(如x86的XOR RAX, RAX或RISC-V的ADD x1, x0, x0)在重命名阶段识别出来,直接将目的寄存器映射到一个硬连线为0的物理寄存器,无需执行。

寄存器重命名(Register Renaming)

寄存器重命名是乱序执行引擎的关键入口。其核心任务是消除指令之间的WAR和WAW假相关性,方法是将指令中引用的架构寄存器(logical register / architectural register)映射到数量更多的物理寄存器(physical register)。

RAT的基本概念

寄存器别名表(Register Alias Table,RAT)是重命名阶段的核心数据结构。RAT维护了从架构寄存器到物理寄存器的映射关系。当一条指令被重命名时,处理如下:

  1. 源操作数查询:查询RAT,获取该指令的每个源操作数当前所映射的物理寄存器编号——这就是该指令应该读取的物理寄存器。例如,如果指令的源寄存器是x5,RAT当前将x5映射到物理寄存器P42,则该指令的源操作数编号被替换为P42

  2. 目的操作数分配:从空闲列表(Free List)中分配一个新的物理寄存器作为该指令的目的操作数。例如,从Free List分配到P67

  3. RAT更新:将该指令的目的架构寄存器的映射更新为新分配的物理寄存器。例如,如果指令的目的寄存器是x5,则RAT中x5的映射从P42更新为P67。旧的映射P42被记录下来(通常存储在ROB表项中),以便在指令退休时释放。

下面用一个具体的例子来说明重命名过程:

asm
# 原始指令              # 重命名后(假设初始映射: x1->P1, x2->P2, x3->P3, x5->P5)
    mul  x1, x2, x3        # mul  P10, P2, P3    (x1映射更新为P10)
    add  x5, x1, x2        # add  P11, P10, P2   (x5映射更新为P11, 使用x1的新映射P10)
    sub  x1, x5, x3        # sub  P12, P11, P3   (x1映射再次更新为P12, 使用x5的新映射P11)
    sd   x1, 0(x2)         # sd   P12, 0(P2)     (使用x1的最新映射P12)

在这个例子中,原始程序中x1被写了两次(MULSUB),这构成WAW相关。经过重命名后,第一次写x1映射到P10,第二次写x1映射到P12,两条指令的目的不再冲突。同样,ADD指令读x1(WAR with SUB),经过重命名后ADDP10(第一次写的结果),SUBP12,不存在冲突。

硬件描述 1 — RAT的硬件实现

RAT有两种主要的硬件实现方式:

(1) 基于SRAM的RAT(也称为RAM-based RAT):使用一个以架构寄存器编号为地址的SRAM表来存储映射关系。对于RISC-V的32个整数寄存器和32个浮点寄存器,整数RAT需要32个表项(x0始终映射到硬连线零寄存器,可省略),每个表项存储一个物理寄存器编号(例如log2280=9\lceil\log_2 280\rceil = 9位)以及一个ready位(表示该物理寄存器中的值是否已经计算完毕)。

一个nn-wide的处理器每周期需要读取2n2n个源操作数的映射(每条指令最多2个源)和写入nn个目的操作数的新映射,因此RAT需要2n2n个读端口和nn个写端口。对于8-wide处理器,这意味着16读8写的多端口SRAM,端口数量较多但在先进工艺下仍然可以在一个时钟周期内完成访问。

(2) 两级RAT结构:Intel的处理器使用一种两级RAT设计。Speculative RAT(也称Front-end RAT,FRAT)跟踪最新的推测性映射——包括分支预测路径上的指令所做的映射更新。Retirement RAT(也称Architecture RAT,ARAT)只跟踪已经提交的指令的映射——代表确定的、不可撤销的架构状态。

当分支预测失败时,FRAT中被错误更新的映射需要被撤销。有两种方式:

  • 将ARAT的全部内容复制到FRAT,然后从ARAT对应的ROB位置开始重新重命名。这种方式恢复简单但速度慢。

  • 在每条分支指令处创建FRAT的快照(checkpoint)。预测失败时直接恢复到对应快照。速度快(1\sim2个周期)但需要为每条飞行中的分支维护一份完整的RAT拷贝,SRAM面积开销约为C×32×9C \times 32 \times 9位(CC为最大同时飞行的分支数,通常约48\sim64条)。

关于RAT的详细设计将在第第 24.0 章\sim第 26.0 章章中深入讨论。

PRF的基本概念

物理寄存器文件(Physical Register File,PRF)是存储所有飞行中指令(in-flight instructions)的操作数值的地方。PRF的条目数决定了处理器可以同时维持多少个活跃的重命名映射——如果物理寄存器耗尽(Free List为空),即使ROB和发射队列还有空间,流水线前端也必须停顿,因为无法为新指令分配目的物理寄存器。

表 2.2列出了几款现代处理器的PRF参数。

处理器整数PRF浮点/SIMD PRF向量PRFROB容量
Intel Golden Cove280332512
Intel Lion Cove304384576
AMD Zen 4224192320
AMD Zen 5240256448
Apple Firestorm380+380+630
Apple Everest400+400+700+
ARM Cortex-X4192192128320
香山昆明湖192192160 (RVV)256

现代处理器的物理寄存器文件参数

从表表 2.2可以看出,Apple的处理器拥有最大的PRF(整数和浮点各超过380/400个),与其巨大的ROB(630/700+项)相匹配。Intel的PRF/ROB比率略低于Apple,表明Intel更依赖于高效的寄存器回收机制来控制PRF的使用效率。AMD Zen系列的PRF相对较小,这与其较小的ROB一致。香山昆明湖在14 nm工艺下受面积限制,PRF和ROB都相对保守。

PRF的数据宽度也是一个重要的设计参数。整数PRF的每个条目通常为64位(对应64位架构寄存器宽度)。但浮点/SIMD PRF的条目宽度取决于ISA支持的最大向量宽度:支持AVX2的处理器需要256位的浮点PRF条目,支持AVX-512的需要512位,支持ARM SVE2的需要可变宽度(128\sim2048位,取决于实现)。更宽的PRF条目意味着更大的SRAM面积和更高的读写功耗。Intel的Golden Cove/Lion Cove支持AVX-512(虽然在某些SKU中被禁用),因此其浮点PRF的每个条目至少为512位宽。

PRF的大小需要满足以下约束。在最坏情况下(ROB中每条指令都写一个不同的目的寄存器),需要的物理寄存器数为:已提交映射需要AR|AR|个(每个架构寄存器一个),飞行中指令需要最多ROB|ROB|个(每条指令一个新的物理寄存器)。因此:

PRFROB+AR |PRF| \geq |ROB| + |AR|

对于RISC-V(32个整数架构寄存器)和256项ROB的处理器,最少需要256+32=288256 + 32 = 288个整数物理寄存器。实际设计中,由于并非每条指令都有目的寄存器(如SDBEQ等指令不写寄存器),且某些指令可能被move消除或零化(不消耗物理寄存器),PRF可以略小于公式[eq:ch02-prf-min]的上界。但过小的PRF会导致频繁的Free List耗尽停顿,通常设计时会留出10%\sim20%的余量。

分发(Dispatch)

经过寄存器重命名后,指令需要被分发(dispatch)到后端的各种硬件结构中。分发阶段是前端和后端之间的桥梁,也是流水线中最容易成为瓶颈的阶段之一,因为它需要同时与多个硬件结构交互。

分发阶段的主要操作包括:

  1. ROB分配:为每条指令在ROB中分配一个表项,记录指令的PC、类型、目的架构/物理寄存器映射、旧的物理寄存器映射(用于退休时释放)、异常信息等。ROB采用循环队列(circular queue)结构,分配指针(tail pointer)每周期推进最多nn项(nn为分发宽度)。

  2. 发射队列分配:将指令插入到对应类型的发射队列(Issue Queue,IQ)中。现代处理器通常将IQ分为多个分区——整数IQ(IQ-INT)、浮点/SIMD IQ(IQ-FP)、访存IQ(IQ-MEM)——以减少每个IQ的大小和复杂度。指令根据其类型被分发到对应的IQ分区。

  3. Store Buffer分配:如果指令是store指令,还需要在Store Buffer(SB)中分配一个表项来暂存其地址和数据,直到该store指令退休时才将数据写入D-Cache。

  4. Load Queue分配:如果指令是load指令,需要在Load Queue(LQ)中分配一个表项来记录其地址和执行状态,用于后续的存储器消歧(memory disambiguation)和存储器顺序违例检测。

  5. 背压检查:如果上述任何一个资源已满(ROB满、IQ满、SB满、LQ满、或Free List空),分发阶段必须向前端施加背压(backpressure),暂停前端的取指和解码,直到资源释放。在实际处理器中,这种暂停是性能损失的重要来源之一。

性能分析 2 — 分发宽度瓶颈分析

分发宽度通常等于解码宽度(即nn-wide处理器每周期分发nn条uop),但实际有效分发宽度可能低于名义宽度。这是因为不同类型的资源有不同的分配带宽限制:

  • ROB分配:通常不是瓶颈——ROB分配逻辑简单,只需要移动tail指针。

  • IQ分区瓶颈:如果某个IQ分区已满,而本周期有多条属于该分区类型的指令,则超出的指令必须等待。例如,如果IQ-MEM只有2个写端口,但本周期有3条load/store指令需要分发到IQ-MEM,则第3条必须等待下一周期。

  • SB/LQ分配瓶颈:SB和LQ通常每周期可分配2\sim3项。在SPEC CPU 2017工作负载中,约15%\sim20%的指令是store指令,10%\sim15%是load指令。对于一个8-wide处理器,每周期平均需要分发1.2\sim1.6条store和0.8\sim1.2条load,这对SB/LQ的分配带宽要求不算太高。但在memcpy等store密集的代码段中,store比例可达50%,可能触发SB分配瓶颈。

在实际设计中,分发阶段的"瓶颈资源"(bottleneck resource)会随工作负载变化。良好的微架构设计应该使得各种资源的大小和分配带宽互相匹配,避免出现某一种资源显著小于其他资源而频繁成为瓶颈的情况。

发射(Issue)

发射阶段是乱序执行引擎的心脏。指令在发射队列(IQ)中等待其所有源操作数就绪,一旦就绪就可以被选择并发射到功能单元执行。这个过程包含三个紧密耦合的操作:等待(wait)、唤醒(wakeup)和选择(select)。

唤醒逻辑

唤醒逻辑监测IQ中每条指令的每个源操作数是否已经就绪。当一条指令在执行单元中完成计算时,其结果的物理寄存器编号会通过唤醒总线(wakeup bus)广播到所有IQ表项。每个IQ表项中的每个源操作数都配有一个比较器(comparator),将广播的物理寄存器编号与自己的源操作数编号进行比较。如果匹配成功,则将对应的源操作数标记为"就绪"(ready)。当一条指令的所有源操作数都就绪后,该指令变为"可发射"(ready-to-issue)状态。

唤醒逻辑的硬件复杂度为O(Wbus×NIQ×Nsrc)O(W_{\text{bus}} \times N_{\text{IQ}} \times N_{\text{src}}),其中WbusW_{\text{bus}}为唤醒总线数量(等于每周期可以完成的指令数),NIQN_{\text{IQ}}为IQ表项数,NsrcN_{\text{src}}为每条指令的源操作数数量(通常为2)。对于一个128项的IQ和12条唤醒总线,每个IQ表项需要进行12×2=2412 \times 2 = 24次比较,整个IQ共需128×24=3072128 \times 24 = 3072个比较器。每个比较器的宽度等于物理寄存器编号的位数(约8\sim9位)。这些比较器的面积和功耗是IQ中最大的开销之一,这也是为什么需要将IQ分区的重要原因——将128项的IQ分为3个分区(INT 48项、FP 40项、MEM 40项),每个分区只需要与该分区对应的唤醒总线比较,总比较器数量大幅减少。

选择逻辑

在每个周期中,可能有多条指令同时变为就绪状态(ready),但每个执行端口只能接受一条指令。选择逻辑(select logic,也称为仲裁逻辑/arbiter)负责从就绪的指令中选出要发射到各个执行端口的指令。常见的选择策略包括:

  • 最老优先(Oldest-First):选择IQ中最老的(即最早进入IQ的)就绪指令。这种策略可以保证不会出现饥饿(starvation),并且倾向于优先执行处于依赖链上游的指令,有利于缩短依赖链的总延迟。但需要在所有就绪指令之间进行年龄比较,朴素实现的复杂度为O(NIQ2)O(N_{\text{IQ}}^2)

  • FIFO选择:按照指令进入IQ的顺序选择,可以用简单的队列指针实现。但FIFO策略可能导致就绪的年轻指令阻塞尚未就绪的老指令之后,降低IPC。

  • 关键路径优先(Critical-Path-First):优先选择处于程序依赖链关键路径上的指令,可以减少整体延迟。但关键路径的识别需要额外的硬件支持(如依赖深度计数器),增加了设计复杂度。

图 2.2展示了唤醒-选择-执行的一个周期内的时序关系。

发射队列中唤醒-选择机制的示意图。唤醒总线广播刚完成的指令的目的物理寄存器编号(P12),IQ中等待P12的表项匹配后将对应源标记为就绪。选择逻辑从所有就绪表项中按年龄优先选择发射到执行端口。
发射队列中唤醒-选择机制的示意图。唤醒总线广播刚完成的指令的目的物理寄存器编号(P12),IQ中等待P12的表项匹配后将对应源标记为就绪。选择逻辑从所有就绪表项中按年龄优先选择发射到执行端口。

现代处理器通常采用最老优先策略的高效硬件实现——基于年龄矩阵(Age Matrix)的选择器。年龄矩阵是一个N×NN \times N的位矩阵,其中位(i,j)(i,j)为1表示指令ii比指令jj更老。最老的就绪指令就是在就绪指令中对应行全为1的那一行。年龄矩阵的更新只在指令进入和离开IQ时发生,不影响选择的关键路径。选择逻辑通过将年龄矩阵的行与就绪向量进行AND操作,然后进行OR归约,可以在O(logN)O(\log N)的延迟内完成选择。

推测唤醒

为了减少唤醒-选择-执行之间的串行延迟,现代处理器广泛使用推测唤醒(speculative wakeup)技术,其中最重要的应用场景是load推测唤醒

当一条load指令被选择发射到LSU(Load/Store Unit)时,处理器乐观地假设该load将在L1D Cache中命中(延迟为4\sim5个周期是已知的固定延迟)。因此,在load指令被选择的同一周期就通过唤醒总线广播该load的目的物理寄存器编号,唤醒那些依赖该load结果的指令。被唤醒的指令会在load预期完成的那个周期被选择发射,通过旁路网络接收load的结果。

这种推测唤醒消除了"load完成\to广播唤醒\to依赖指令被选择"的多周期串行延迟,在load密集的工作负载中可以显著提高IPC。

然而,如果load指令实际在L1D Cache中缺失(需要更长的延迟访问L2 Cache或更下层),则那些被推测唤醒并已经开始执行的依赖指令使用了错误的(尚未就绪的)数据。此时需要进行重放(replay):将这些被错误唤醒的指令重新送回IQ,将它们的源操作数标记为"未就绪",等待load从L2 Cache返回正确数据后再次唤醒。

设计权衡 2 — 推测唤醒的收益与代价

推测唤醒的收益取决于L1D Cache的命中率。在典型的SPEC CPU工作负载中,L1D Cache命中率约为90%\sim95%,这意味着90%\sim95%的load推测唤醒是正确的。对于这些正确的推测唤醒,每条依赖指令节省了2\sim3个周期的延迟(唤醒广播+选择的时间)。对于5%\sim10%的错误推测唤醒,需要进行重放,代价约为5\sim10个周期(等待L2 Cache返回+重新唤醒+重新选择+重新执行)。

综合来看,推测唤醒的净收益通常为正——它在L1 Cache命中率>>80%的场景下都是有利的。但在L1 Cache命中率低的工作负载中(如指针密集的数据结构遍历),频繁的重放可能抵消推测唤醒的收益,甚至导致性能下降。一些处理器采用了自适应的推测唤醒策略:跟踪每条load指令的L1 Cache命中历史,只对高命中率的load启用推测唤醒。

读取寄存器(Register File Read)

指令被选择发射后,需要从物理寄存器文件(PRF)中读取源操作数的值。PRF的读端口数量是后端硬件设计中的一个重要约束,直接影响PRF的面积、延迟和功耗。

对于一个拥有pp个执行端口的后端,每条指令最多有2个源操作数(部分指令如FMA有3个源),因此PRF需要2p2p3p3p个读端口。对于一个12端口的后端,这意味着24\sim36个读端口——如此多的端口会使PRF的面积和延迟急剧增加。多端口SRAM的面积大致与端口数的平方成正比:

APRFNentries×Wdata×(R+W)2A_{\text{PRF}} \propto N_{\text{entries}} \times W_{\text{data}} \times (R + W)^2

其中RRWW分别为读端口和写端口数量,NentriesN_{\text{entries}}为PRF条目数,WdataW_{\text{data}}为每个条目的数据宽度(64位整数或128/256/512位SIMD)。因此从12端口增加到16端口可能导致PRF面积增加((32+16)2/(24+12)21)78%((32+16)^2/(24+12)^2 - 1) \approx 78\%(此处假设读写端口各增加4个)。

为了缓解这个问题,现代处理器采用了以下技术:

  • PRF分区(PRF Partitioning):将PRF分为整数PRF和浮点/SIMD PRF,各自拥有独立的读写端口。由于整数指令和浮点指令通常不共享源操作数(跨类型的数据移动指令除外),这种分区可以将每个PRF的端口需求减半。进一步地,可以为向量/SIMD指令单独设置一个宽数据的向量PRF。

  • Banked PRF:将PRF按物理寄存器编号进一步划分为多个Bank(例如奇数编号为Bank 0,偶数编号为Bank 1)。每个Bank拥有较少的端口,不同指令访问不同Bank时可以并行进行。只有当多条指令需要同时访问同一个Bank时才会发生Bank冲突(bank conflict),需要stall或仲裁。Bank冲突的概率可以通过物理寄存器的分配策略来降低(例如交替从不同Bank分配寄存器)。

  • 将读寄存器与发射合并:在某些设计中(如ARM Cortex-A77和部分学术原型),选择逻辑和PRF读取在同一个时钟周期内完成:选择逻辑在周期的前半部分完成(半周期操作),PRF读取在后半部分完成。这节省了一个流水段,但对时序约束更加严格,需要仔细的物理设计。

  • 旁路替代读取:如果指令的源操作数来自上一周期刚刚执行完毕的指令,可以通过旁路网络(bypass network)直接获取结果,而不需要从PRF读取。这减轻了PRF读端口的实际利用率(虽然端口数量不能减少,因为必须保证最坏情况下所有指令都需要从PRF读取)。

执行(Execute)

执行阶段是指令实际进行运算的阶段。现代高性能处理器配备了多种类型的功能单元(Functional Unit,FU),以支持不同类型的指令。不同FU的延迟和吞吐量差异很大,这对发射调度和旁路网络的设计都有深刻的影响。

案例研究 1 — Intel Golden Cove的执行端口配置

Intel Golden Cove(用于第12代Alder Lake处理器的P-core)拥有12个执行端口,配置如下:

  • 端口0:整数ALU、整数快速LEA、整数乘法/除法、AES-NI加密单元、浮点FMA

  • 端口1:整数ALU、整数快速LEA、整数乘法、浮点FMA、向量ALU

  • 端口2:Load单元(每周期执行1条128位或256位加载)

  • 端口3:Load单元(每周期执行1条128位或256位加载)

  • 端口4:Store数据端口(将数据写入Store Buffer)

  • 端口5:整数ALU、向量Shuffle/Permute、浮点FMA

  • 端口6:整数ALU、分支执行单元(Branch Unit)

  • 端口7:Store地址计算(AGU, Address Generation Unit)

  • 端口8:Store地址计算(AGU)

  • 端口9:Store数据端口

  • 端口10:整数ALU

  • 端口11:整数ALU

关键观察:

  • 整数ALU拥有6个端口(0/1/5/6/10/11),每周期最多执行6条整数ALU指令——这超过了6-wide解码宽度,保证了在有足够ILP时ALU不会成为瓶颈。

  • 浮点FMA有3个端口(0/1/5),每周期最多执行3条FMA指令,理论浮点吞吐量为3×2×2=123 \times 2 \times 2 = 12个双精度FLOP/周期(每条FMA执行乘加两个操作,每条处理256位即4个双精度数)。

  • Load有2个端口(2/3),Store的地址和数据各有2个端口,因此每周期最多2 Load + 2 Store。

表 2.3列出了各类指令在典型现代处理器中的执行延迟和吞吐量。

指令类型延迟(周期)吞吐量(条/周期)流水化?
整数加法/逻辑/移位14–6
整数乘法 (64-bit)3–41–2
整数除法 (64-bit)12–40<<1
浮点加法 (DP)3–42–3
浮点乘法/FMA (DP)4–52–3
浮点除法 (DP)10–20<<1部分
浮点平方根 (DP)15–25<<1部分
L1D Cache加载4–52–3
SIMD整数运算 (256-bit)12–3
SIMD浮点FMA (512-bit)4–51–2
分支11–2

典型现代处理器的功能单元延迟和吞吐量

"流水化"(pipelined)意味着FU虽然需要多个周期完成一条指令,但每个周期可以接受一条新指令进入流水线。例如一个4周期延迟的流水化乘法器在内部有4级流水段,在稳态下可以同时处理4条乘法指令(每条处于不同的流水段),每周期输出一条结果。而"非流水化"的除法器在处理一条除法指令期间无法接受新的除法指令——除法器被"占用"直到当前指令完成,后续的除法指令必须在IQ中等待。

值得注意的是,L1D Cache加载的延迟(4\sim5个周期)是现代处理器中最关键的延迟之一。几乎所有需要从内存读取数据的指令链都会经过load指令,load延迟直接影响了pointer chasing(指针追踪)代码的性能。在链表遍历、树搜索等工作负载中,每次load的结果是下一次load的地址,形成了loadaddress computationload\text{load} \to \text{address computation} \to \text{load} \to \ldots的长依赖链,每次迭代的延迟至少等于load延迟(4\sim5个周期)。这也是为什么Apple的处理器将L1D Cache加载延迟优化到3个周期(在某些简单寻址模式下)——仅此一项优化就可以在pointer chasing密集的工作负载中带来可观的IPC提升。

案例研究 2 — AMD Zen 5的执行端口配置与Zen 4的对比

AMD Zen 5(2024年)相比Zen 4(2022年)在后端进行了显著的扩展:

  • 整数ALU:Zen 4有4个ALU端口,Zen 5增加到6个。这使得整数ALU的吞吐量提升了50%,对于ALU密集的代码(如编译器、加密算法)效果显著。

  • 整数乘法器:Zen 4有1个乘法端口(延迟3周期),Zen 5增加到2个。

  • 分支执行:Zen 4有1个分支端口,Zen 5增加到2个。这在分支密集的代码中减少了分支执行端口的冲突。

  • Load/Store:Zen 4有3个AGU端口(2 Load + 1 Store),Zen 5仍然是3个AGU端口但优化了调度。

  • 浮点/SIMD:Zen 4有4个浮点端口(2 FMA + 2 FADD/Shuffle),Zen 5维持4个但扩展到全宽AVX-512支持。

Zen 5的执行端口总数从Zen 4的10个增加到12个,同时解码宽度从6-wide增加到8-wide。这种后端和前端的同步扩展是平衡设计的良好示范——如果只扩展前端而不扩展后端,前端供给的指令将在IQ中堆积;如果只扩展后端而不扩展前端,执行端口将经常空闲。

图 2.3以可视化的方式对比了几款处理器的执行端口配置。

现代处理器执行端口配置对比(简化,部分端口可共享多种功能)。A=整数ALU,L=Load,S=Store,F=浮点/SIMD,B=分支。
现代处理器执行端口配置对比(简化,部分端口可共享多种功能)。A=整数ALU,L=Load,S=Store,F=浮点/SIMD,B=分支。

写回(Write Back)

指令在功能单元中执行完毕后,进入写回阶段。写回阶段的核心任务有两个:将结果写入PRF,以及通过旁路网络将结果前递给等待的指令。

物理寄存器文件写入

将执行结果写入PRF中该指令被分配的目的物理寄存器。PRF的写端口数量需要等于每周期可以产生结果的最大指令数量。对于12端口的后端,理论上需要12个写端口,但实际上并非所有端口在每个周期都会产生结果(例如store指令不写PRF,多周期指令在中间流水段不产生结果),因此可以通过写端口共享和仲裁来减少物理写端口数量。一种常见的方法是让两个低利用率的端口共享一个物理写端口,在冲突时仲裁其中一个延迟一周期写入。

旁路网络

旁路网络(Bypass Network,也称为Forwarding Network)是写回阶段最重要的硬件结构,也是整个后端面积和功耗的重要贡献者。旁路网络将执行结果在写入PRF的同时直接前递到正在等待该结果的指令,使得依赖指令可以在下一个周期(而非等待PRF写入完成后的下下个周期)就开始执行。

对于背靠背(back-to-back)执行场景——即一条1周期延迟的ALU指令的结果被下一条ALU指令使用——旁路网络的工作方式如下:

asm
add  x1, x2, x3    # 周期N: 执行, 结果通过旁路网络前递
    sub  x4, x1, x5    # 周期N+1: 通过旁路网络获取x1的值, 立即执行

如果没有旁路网络,SUB指令必须等待ADD的结果先写入PRF(周期N+1),然后在下一个周期从PRF读出(周期N+2),再执行(周期N+3)——总延迟为3个周期而非1个周期。旁路网络消除了PRF的读写延迟,将back-to-back ALU操作的吞吐量从每3周期1条提升到每周期1条。

旁路网络的复杂度为O(P2)O(P^2),其中PP为执行端口数量。这是因为任何一个端口产生的结果都可能被其他任何端口上即将执行的指令所需要。对于12个端口,旁路网络需要12×12=14412 \times 12 = 144条前递路径。每条路径包含一个数据宽度的MUX(64位整数或128\sim512位SIMD)和一条跨越执行引擎的物理连线。在先进工艺下,这些长连线的延迟可能成为时钟周期的瓶颈。

设计权衡 3 — Clustered微架构

为了控制旁路网络的复杂度和连线延迟,一些处理器采用了Cluster结构(也称为分簇结构)。在这种结构中,执行端口被分为2\sim4个Cluster,每个Cluster内部有完整的旁路网络(前递延迟为0,即同周期可用),Cluster之间的前递需要额外1个周期的延迟。

例如,一个12端口的后端分为2个Cluster(每个6端口),Cluster内部的旁路网络从122=14412^2 = 144条路径减少到62×2=726^2 \times 2 = 72条,连线长度也大幅缩短。Alpha 21264是最早采用Cluster结构的处理器之一,它将整数执行端口分为2个Cluster,每个Cluster有2个ALU端口和1个Load/Store端口。AMD Zen系列的整数后端也采用了类似的Cluster结构。

Cluster结构的性能代价是:如果一条指令的源操作数来自另一个Cluster中刚完成的指令,需要额外等待1个周期的跨Cluster传输延迟。这对于紧密耦合的依赖链可能造成可观的IPC损失。编译器和硬件调度器可以通过将相关指令分配到同一个Cluster来减少跨Cluster的数据传输。关于Cluster结构的详细分析将在第第 34.0 章章中讨论。

提交(Commit)

提交阶段(也称为退休阶段,Retire)是乱序执行恢复到顺序语义的关键环节。这是超标量处理器维持精确异常(precise exception)和精确中断(precise interrupt)语义的基础。

ROB按照程序的原始顺序检查队头的指令是否满足退休条件:

  1. 指令已经在执行阶段完成(execution_complete标志被置位);

  2. 指令没有触发异常(exception标志未被置位);

  3. 指令之前的所有指令都已退休(这是ROB循环队列结构自然保证的——只有队头指令才会被检查)。

当这些条件都满足时,指令从ROB的队头退休(head指针推进)。退休意味着该指令的结果成为架构状态(architectural state)的一部分——对外部可见、不可撤销。具体操作包括:

  • 更新ARAT:将该指令的目的架构寄存器在ARAT中的映射更新为该指令分配的物理寄存器。

  • 释放旧物理寄存器:将该指令之前的旧映射所对应的物理寄存器释放回Free List。例如,如果x5之前映射到P42,现在被该指令更新为映射到P67,则P42被释放。注意:P42不能在该指令分发时就释放,因为在分发和退休之间可能有其他指令还在使用P42的值(作为源操作数)。只有当该指令退休后,才能确定P42不再被任何指令需要。

  • Store退休(Store Commit):如果是store指令,允许Store Buffer中对应的表项将数据写入D-Cache。在退休之前,store的数据只存在于Store Buffer中,对其他核心不可见。退休后,store的数据才会被写入Cache层次结构,变得对其他核心可见(在适当的一致性协议操作之后)。

  • 分支预测器训练:如果是分支指令,将其实际方向和目标地址反馈给分支预测器,用于更新预测器的历史表和计数器。

ROB的硬件实现通常采用循环队列(circular buffer)结构,使用两个指针来管理:

  • 尾指针(tail pointer):指向下一个可分配的空表项。每周期分发nn条指令时,尾指针推进nn项。

  • 头指针(head pointer):指向最老的未退休指令。每周期退休kk条指令时,头指针推进kk项。

  • 当头指针追上尾指针时,ROB为空(所有指令已退休)。

  • 当尾指针追上头指针时,ROB满(无法接受新指令,前端停顿)。

ROB表项的典型内容包括:指令PC(用于异常报告和调试)、指令类型、目的架构寄存器编号、目的物理寄存器编号、旧的物理寄存器编号(退休时释放)、completed标志、exception标志及异常类型码、分支预测结果(用于退休时训练预测器)等。每个ROB表项的宽度约为50\sim80位,512项ROB的总SRAM容量约为3\sim5KB。

提交宽度(commit width/retire width)通常与分发宽度相同或更宽。例如Intel Golden Cove每周期可以退休最多8条uop(与其6-wide解码宽度相比更宽),AMD Zen 4每周期可以退休最多8条uop。更宽的提交宽度可以避免在指令突发完成时ROB队头出现堆积,导致ROB满而触发前端停顿。

硬件描述 2 — 异常处理与状态恢复

当一条指令在执行阶段触发异常(如除零、缺页异常/page fault、非法指令、地址未对齐等)时,该异常不会立即被处理。异常信息被记录在ROB的对应表项中(设置exception标志和异常类型码)。指令仍然被标记为"完成",以允许其后续可以到达ROB队头。只有当该指令到达ROB队头准备退休时,异常才会被真正触发。此时:

  1. ROB执行清空(flush)操作:丢弃该指令及其之后的所有飞行中指令;

  2. Speculative RAT恢复为Architecture RAT的状态——因为ARAT只包含已退休指令的映射,代表正确的架构状态;

  3. 所有被清空指令占用的物理寄存器被释放回Free List(通过将Free List恢复到ARAT中记录的状态);

  4. 发射队列、Store Buffer、Load Queue中属于被清空指令的表项被清除;

  5. PC被设置为异常处理程序的入口地址(通过查询异常向量表)。

这种"延迟到退休时处理异常"的机制保证了精确异常(precise exception):异常发生时,该异常指令之前的所有指令都已经提交(结果对架构层面可见),该异常指令及其之后的所有指令都没有提交(结果对架构层面不可见),与程序按顺序执行的效果完全一致。这是操作系统正确实现虚拟内存(通过缺页异常)、调试器正确报告异常位置等功能的基础。

一个例外是机器检查异常(Machine Check Exception,MCE),这是由不可恢复的硬件错误(如ECC不可纠正的内存错误)触发的。MCE不需要等到退休才处理,因为此时处理器状态已经被破坏,无论如何都无法保证精确恢复。MCE通常触发立即的流水线清空和错误处理程序。

流水线阶段的时序总结

综合以上各阶段的讨论,图图 2.4展示了一条指令从进入流水线到退休的完整时序。以一条load指令为例,在一个典型的20级流水线处理器中,它经历的各阶段时序如下。

一条Load指令在20级流水线中的完整时序。BP=分支预测, IF=取指, PD=预解码, DC=解码, RN=重命名, DI=分发, IS=发射, RF=读寄存器, AG=地址生成, TLB=TLB查询, DC=D-Cache访问, DT=数据返回, WB=写回, CM=提交。
一条Load指令在20级流水线中的完整时序。BP=分支预测, IF=取指, PD=预解码, DC=解码, RN=重命名, DI=分发, IS=发射, RF=读寄存器, AG=地址生成, TLB=TLB查询, DC=D-Cache访问, DT=数据返回, WB=写回, CM=提交。

从图图 2.4可以看出,一条load指令从进入流水线(周期0)到结果可以通过旁路网络供给后续指令使用(周期15),需要经历15\sim16个流水段。这也正是分支预测失败惩罚的来源——如果这条指令是一条分支指令,在周期14(执行阶段)才能确定预测是否正确,此时周期1\sim14之间已经取出和处理了大量推测性指令。

流水线宽度的选择

流水线宽度(pipeline width)——即处理器每周期可以取指、解码、重命名、分发和提交的指令/微操作数——是超标量处理器最基本的设计参数之一。从历史上看,通用处理器的宽度经历了从2-wide(Pentium,1993年)到4-wide(Core 2,2006年)、6-wide(Zen 3/Golden Cove,2020\sim2021年)、8-wide(Firestorm/Zen 5/Lion Cove,2020\sim2024年)再到10-wide(Everest,2024年)的演进过程。本节分析宽度选择背后的设计权衡。

宽度对IPC的影响

理论上,一个nn-wide的处理器每周期最多可以提交nn条指令,即理想IPC等于nn。但实际IPC远低于这个上限。回顾第 1.0 章中CPI的组成——CPItotal=CPIbase+CPIbranch+CPIcache+CPIother\text{CPI}_{\text{total}} = \text{CPI}_{\text{base}} + \text{CPI}_{\text{branch}} + \text{CPI}_{\text{cache}} + \text{CPI}_{\text{other}},增加宽度主要降低CPIbase\text{CPI}_{\text{base}}(从1/41/4降到1/61/6再到1/81/8),但其余分量不随宽度线性改善,导致总体收益递减。具体来说,以下几种限制约束着实际IPC:

  1. 数据依赖(Data Dependence):程序中的RAW依赖形成依赖链,限制了可以并行执行的指令数量。即使有无限宽的处理器,IPC也无法超过依赖链所允许的上限。对于典型的整数程序,由于大量的指针操作和条件判断,依赖链较密集,可挖掘的ILP有限。

  2. 控制依赖(Control Dependence):分支指令打断了指令流的连续性。taken分支使得取指块中分支之后的指令无效,降低了有效取指带宽。分支预测失败则导致流水线清空,浪费大量周期。

  3. Cache缺失:I-Cache和D-Cache缺失导致流水线停顿,此时增加宽度无法提供额外的收益。一次L2 Cache缺失(30\sim50周期)可能浪费掉3050×n30\sim50 \times n条指令的执行机会。

  4. 资源冲突:有限的执行端口数量(特别是load/store端口、除法器等稀缺资源)限制了实际并行度。

  5. 有限的指令窗口:ROB和IQ的容量限制了处理器同时跟踪的指令数量。如果ROB太小,处理器无法"看到"足够远的指令来发掘ILP。

图 2.5展示了在典型的SPEC CPU工作负载下,IPC随流水线宽度增加的变化趋势(假设ROB、IQ等资源按比例增大以匹配宽度)。

IPC随流水线宽度的变化(归一化到4-wide,假设ROB/IQ等资源按比例增大)。数据基于多项学术研究和工业报告的综合估计。
IPC随流水线宽度的变化(归一化到4-wide,假设ROB/IQ等资源按比例增大)。数据基于多项学术研究和工业报告的综合估计。

从图图 2.5可以看出,IPC随宽度的增长呈现明显的边际收益递减(diminishing returns)。从4-wide增加到6-wide,SPEC INT的IPC提升约20%;从6-wide增加到8-wide,IPC仅提升约11%;从8-wide增加到10-wide,IPC提升更是不到6%。服务器工作负载的递减趋势更加明显——因为服务器程序通常包含大量的load-dependent分支和指针追踪操作,依赖链更密集,可用ILP更少。

这种递减趋势可以用以下直觉来理解:在一个典型的程序中,可用的ILP是有限的。当处理器宽度较窄时,ILP的瓶颈在于处理器本身——程序中有足够多的并行指令,但处理器每周期只能执行少量。随着宽度增加,瓶颈逐渐从处理器转移到程序本身——即使处理器能每周期执行更多指令,程序中也没有那么多可以并行的指令。

性能分析 3 — Amdahl定律视角下的宽度收益

可以从Amdahl定律的角度来定量理解宽度收益的递减。假设一个程序的执行时间中有ff的比例受限于串行依赖链(无法通过增加宽度来加速),1f1-f的比例可以完全并行化。则nn-wide处理器相对于1-wide处理器的加速比为:

Speedup(n)=1f+1fn \mathrm{Speedup}(n) = \frac{1}{f + \frac{1-f}{n}}

nn \to \infty时,Speedupmax=1/f\mathrm{Speedup}_{\max} = 1/f。对于典型的整数程序,f0.30.4f \approx 0.3\sim0.4(约30%\sim40%的执行时间受限于串行依赖链),因此Speedupmax2.53.3\mathrm{Speedup}_{\max} \approx 2.5\sim3.3,对应于IPCmax2.53.3\mathrm{IPC}_{\max} \approx 2.5\sim3.3条/周期(相对于理想1-wide处理器的IPC=1\mathrm{IPC}=1)。这意味着即使处理器有无限的宽度和无限的资源,整数程序的IPC也不可能超过约3.3条/周期。

对于浮点和向量密集的程序(如科学计算、矩阵运算),由于规则的数据并行性,ff更小(0.150.25\approx 0.15\sim0.25),Speedupmax47\mathrm{Speedup}_{\max} \approx 4\sim7,宽度增加的收益更大。这也解释了为什么图图 2.5中SPEC FP的曲线斜率比SPEC INT更大。

实际的ff值取决于程序的特征、编译器优化水平以及处理器的指令窗口大小(更大的ROB可以"看到"更远的指令,发掘更多并行性,等效于降低ff)。因此ff不是程序的固有属性,而是程序-处理器交互的结果。

下面用一个具体的指令序列来说明宽度如何影响IPC。考虑以下RISC-V代码:

asm
# 假设 x10 指向数组 A, x11 指向数组 B, x12 = 循环计数
loop:
    ld   x1, 0(x10)       # I1: 加载 A[i]
    ld   x2, 0(x11)       # I2: 加载 B[i]
    mul  x3, x1, x2       # I3: A[i] * B[i]        (依赖 I1, I2)
    add  x4, x4, x3       # I4: sum += A[i]*B[i]   (依赖 I3, I4_prev)
    addi x10, x10, 8      # I5: A 指针递增
    addi x11, x11, 8      # I6: B 指针递增
    addi x12, x12, -1     # I7: 计数器递减
    bnez x12, loop         # I8: 循环分支           (依赖 I7)

在这个循环中,关键依赖链是I1I3I4\text{I1} \to \text{I3} \to \text{I4}(load延迟4周期 + mul延迟3周期 + add延迟1周期 = 8周期),以及跨迭代的I4prevI4\text{I4}_{\text{prev}} \to \text{I4}(1周期)。理论上每次迭代至少需要8个周期(受限于乘法的延迟链),在此期间可以执行8条指令,因此理想IPC为8/8=1.08/8 = 1.0条/周期——远低于一个8-wide处理器的理论极限8.0。

但如果处理器的指令窗口足够大(ROB \geq 64项),可以同时看到多个循环迭代:第ii次迭代的I3在等待乘法结果的3个周期内,第i+1i+1次迭代的I1和I2可以开始执行(它们与第ii次的I3没有依赖)。通过跨迭代的指令重叠,实际IPC可以显著高于1.0——在足够宽(\geq4-wide)和足够深的指令窗口下,IPC可以接近8/max(4,1)=2.08 / \max(4, 1) = 2.0条/周期(受限于L1 Cache加载延迟4周期,每次迭代有2条load指令需要在4周期延迟后才能使用结果)。

这个例子说明了一个关键观点:宽度的收益取决于指令窗口是否能暴露跨迭代的并行性。如果ROB太小(例如只有32项,仅能容纳4次迭代),可用的ILP就很有限,增加宽度的收益微乎其微。

宽度对硬件复杂度的影响

增加流水线宽度的代价不仅仅是"多加几个解码器"那么简单。超标量处理器中几乎每个部件的复杂度都与宽度有关,而且很多是超线性关系。表表 2.4总结了各主要部件的复杂度与宽度的关系。

部件关键操作复杂度
解码器 (RISC)nn个独立解码器O(n)O(n)
解码器 (x86)指令边界识别+解码O(nlogn)O(n \log n)O(n2)O(n^2)
RAT读端口2n2n读 + nn写 SRAMO(n2)O(n^2)
RAT内联依赖同周期nn条指令间的转发O(n2)O(n^2)
发射队列唤醒每表项与nn条总线比较$O(n \times
发射队列选择从就绪指令中选pp$O(
PRF端口2p2p读 + ppO(p2)O(p^2)
旁路网络p×pp \times p前递路径O(p2)O(p^2)
ROB端口nn写 + nnO(n)O(n)
存储器消歧Load-Store地址比较$O(

超标量处理器各部件复杂度与宽度nn的关系

综合来看,将处理器宽度从nn增加到n+Δnn+\Delta n,核心面积的增长大致为超线性的。一个粗略的经验法则是:宽度每增加1-wide,核心面积增加约10%\sim15%(RISC)或15%\sim25%(x86),其中旁路网络和RAT是面积增长的主要贡献者。

功耗方面,由于面积增大意味着更多的晶体管(更大的CC)以及更高的活动因子(每周期有更多的逻辑在翻转),动态功耗也与宽度超线性相关。一个粗略的估计是:宽度每增加1-wide,动态功耗增加约12%\sim20%,而IPC仅增加约5%\sim10%(在6-wide以上时)。因此,每瓦性能(Performance/Watt)在宽度超过某个阈值后开始下降。

性能分析 4 — 宽度增加的能效分析

假设处理器核心的功耗与宽度的关系为P(n)n1.5P(n) \propto n^{1.5}(面积超线性增长加上活动因子增大的综合效应),IPC与宽度的关系近似为IPC(n)n0.5\mathrm{IPC}(n) \propto n^{0.5}(边际收益递减的经验公式),则能效(Performance/Power)为:

能效(n)=IPC(n)P(n)n0.5n1.5=1n\text{能效}(n) = \frac{\mathrm{IPC}(n)}{P(n)} \propto \frac{n^{0.5}}{n^{1.5}} = \frac{1}{n}

这意味着从纯能效角度看,窄处理器比宽处理器更高效。例如,2个4-wide核心的总能效高于1个8-wide核心——前者的总IPC为2×IPC(4)=2×40.5=4.02 \times \mathrm{IPC}(4) = 2 \times 4^{0.5} = 4.0(归一化),总功耗为2×41.5=16.02 \times 4^{1.5} = 16.0,能效为4.0/16.0=0.254.0/16.0 = 0.25;后者的IPC为80.5=2.838^{0.5} = 2.83,功耗为81.5=22.68^{1.5} = 22.6,能效为2.83/22.6=0.1252.83/22.6 = 0.125

这正是大小核(big.LITTLE / Performance-Efficiency)异构架构的理论基础:将芯片面积和功耗预算分配给多个窄的效率核(如4-wide的ARM A520或Intel Gracemont)来处理并行度高的工作负载,只在需要单线程高性能时才激活少数宽的性能核(如8-wide的ARM X4或Intel Lion Cove)。这一话题将在第第 46.0 章章中详细讨论。

宽度对频率的影响

增加宽度不仅增加面积和功耗,还可能降低时钟频率。原因在于:

  1. 旁路网络延迟增长:更多的执行端口意味着更长的旁路连线和更大的MUX。在3 nm工艺下,一条跨越整个执行引擎的连线延迟可能超过100 ps,占据一个5 GHz时钟周期(200 ps)的一半以上。如果旁路网络的延迟超过了一个时钟周期的时序余量,要么降低频率,要么在旁路路径上插入额外的流水级(这会增加back-to-back执行的延迟,降低IPC)。

  2. 选择逻辑延迟增长:从更多的就绪指令中选择需要更深的优先级编码器树或更大的年龄矩阵,增加了选择逻辑的关键路径延迟。唤醒-选择-旁路读取的串行路径是许多处理器中频率受限的关键路径之一。

  3. 重命名逻辑延迟增长nn条同时重命名的指令之间可能存在内联依赖(intra-group dependency)。例如,如果本周期的第1条指令写x5,第3条指令读x5,则第3条指令的源操作数必须使用第1条指令分配的新物理寄存器编号(而非RAT中的旧映射)。这种内联依赖的检测和转发需要O(n)O(n)级的比较和MUX链:第2条指令需要检查第1条的目的寄存器,第3条需要检查第1条和第2条的目的寄存器,以此类推。这条链的延迟与nn成线性关系。

案例研究 3 — Apple处理器的宽度与频率策略

Apple的处理器采用了业界最宽的设计(Firestorm 8-wide,Everest 10-wide),但时钟频率相对保守(Firestorm 3.2 GHz,Everest 4.4 GHz)。相比之下,Intel Lion Cove的8-wide设计运行在5.5 GHz以上,AMD Zen 5的8-wide设计运行在5.8 GHz。

Apple能做到超宽设计的原因包括:

  • ISA优势:使用AArch64指令集,解码简单,不需要复杂的指令边界识别逻辑和微码ROM。每增加一个解码器的边际成本很低。

  • 工艺领先:Apple一直使用TSMC最新的工艺节点(M1用N5,M3用N3,M4用N3E),在同代工艺中拥有最高的晶体管密度和最优的互连性能。

  • 面积不受限:Apple的移动SoC由Apple自己设计并由TSMC代工,不需要与其他厂商共享Die面积。Apple可以为CPU核心分配充足的面积预算(单个P-core面积约4 mm2@N3,大于ARM Cortex-X4的3 mm2@N4)。

  • 低频高IPC策略:较低的频率意味着每一级流水线有更充裕的时序余量(Firestorm 3.2 GHz对应312 ps/级 vs. Golden Cove 5.2 GHz对应192 ps/级),可以在一级流水线内完成更复杂的逻辑,或者在相同逻辑复杂度下使用更少的流水级。

Apple这种"低频高IPC"的策略在移动场景下特别有优势:较低频率允许使用更低的电压(VDDV_{\text{DD}}ff在亚阈值区域近似成正比),而功耗与V2fV^2 f成正比,因此降低频率20%可以降低功耗约50%。以每瓦性能(performance per watt)衡量,Apple的处理器长期处于业界领先地位。

然而,在追求最高绝对性能(如数据中心服务器场景)时,Intel和AMD的高频策略可能更有优势——它们通过更深的流水线和更精细的物理设计在5.5 GHz以上运行,虽然IPC略低但频率优势可以弥补。

现代处理器的宽度选择

表 2.5总结了截至2025年的几款现代高性能处理器核心的宽度选择及其关键微架构参数。

处理器ISA解码宽度执行端口IQ总容量ROB频率工艺
AMD Zen 4x86610\sim1283205.7 GHzN5
AMD Zen 5x86812\sim1604485.8 GHzN4
Intel Golden Covex86612\sim1605125.2 GHzIntel 7
Intel Lion Covex86812\sim1925765.5 GHzIntel 4
Apple FirestormAArch64814\sim2006303.2 GHzN5
Apple EverestAArch641016\sim240700+4.4 GHzN3E
ARM Cortex-X4AArch64610\sim1603203.4 GHzN4
香山昆明湖RISC-V68\sim1282562.5 GHzN14

现代处理器的宽度选择与关键微架构参数

从表表 2.5可以观察到以下趋势:

第一,宽度在稳步增加。2020年的主流高性能核心为6-wide,到2024年8-wide已成为新的标准(AMD Zen 5、Intel Lion Cove),Apple甚至达到了10-wide。这说明工艺进步(更高的晶体管密度、更低的互连延迟)使得更宽设计的面积和功耗代价变得可以接受。每一代工艺节点的晶体管密度大约提升70%\sim80%,这为增加微架构宽度提供了面积预算。

第二,执行端口数通常大于解码宽度。Golden Cove的解码宽度为6-wide但有12个执行端口,Apple Firestorm的解码宽度为8-wide但有14个执行端口。这种不对称设计有两个原因:(a) 由于微操作缓存命中、指令融合等优化,后端在某些周期可能收到多于解码宽度的uops;(b) 多个执行端口可以减少功能单元冲突的概率——当多条指令需要同类型FU时,有更多的端口可以选择。

第三,ROB和IQ随宽度同步增大。更宽的处理器需要更大的指令窗口来发掘足够的ILP。如果ROB太小(相对于宽度),指令窗口受限,宽度的收益将被抵消。一个经验法则是:ROB容量大约为宽度的50\sim80倍(ROB50n80n|ROB| \approx 50n \sim 80n)。例如,6-wide的Zen 4有320项ROB(53n\approx 53n),8-wide的Firestorm有630项ROB(79n\approx 79n),10-wide的Everest有700+项ROB(70n\approx 70n)。

第四,ISA对宽度扩展的影响显著。AArch64和RISC-V的固定长度编码使得解码器可以简单地线性扩展,Apple因此能将宽度推到10-wide。x86的变长编码使得解码器的扩展成本更高,但通过微操作缓存来缓解——在热点代码中,解码器宽度不是瓶颈(微操作缓存直接供给后端),因此x86处理器的有效宽度也可以达到8-wide以上。

第五,频率与宽度的权衡因厂商而异。Apple(10-wide, 4.4 GHz)和Intel(8-wide, 5.5 GHz)在IPC×f\mathrm{IPC}\times f的乘积上大致相当,但达到这个乘积的路径截然不同。未来的趋势可能是在先进工艺(2 nm及以下)的推动下,继续增加宽度的同时维持甚至提高频率。TSMC的N2工艺(预计2025年量产)引入了GAA(Gate-All-Around)纳米片晶体管,有望在面积、功耗和性能上同时改善,为12-wide甚至更宽的设计提供工艺基础。

展望2030年代,高性能处理器核心的宽度有望达到12\sim16-wide。要实现如此大的宽度,需要在以下几个方面取得突破:(1) 更精确的分支预测器(失败率<<1%),以充分供给更宽的后端;(2) 值预测的商用化,以打破依赖链对IPC的限制;(3) Cluster化的后端设计,以控制旁路网络的复杂度;(4) 更大的ROB(>>1000项)和IQ(>>300项),以提供足够的指令窗口来发掘ILP。

流水线深度的选择

流水线深度(pipeline depth)是另一个至关重要的微架构参数。更深的流水线可以将每一级的逻辑延迟减小,从而允许更高的时钟频率;但更深的流水线也增加了流水线寄存器的面积和功耗开销,并且加大了分支预测失败的惩罚。本节从数学模型出发,分析流水线深度的最优选择,并通过历史案例说明过深流水线的教训。

Cost/Performance的数学分析

沿用第第 1.0 章章引入的Cost/Performance模型。假设不使用流水线时,整个处理器后端的组合逻辑延迟为DD(从输入到输出的最长路径),硬件面积为GG。使用nn级流水线后,理想情况下每级的逻辑延迟为D/nD/n(假设逻辑可以被均匀地切割),但每级还需要额外的流水线寄存器延迟SS,包括:

S=tsetup+tclk-to-q+tskewS = t_{\text{setup}} + t_{\text{clk-to-q}} + t_{\text{skew}}

其中tsetupt_{\text{setup}}为寄存器的建立时间,tclk-to-qt_{\text{clk-to-q}}为时钟到输出延迟,tskewt_{\text{skew}}为时钟偏斜余量。在3 nm工艺下,SS的典型值约为15 ps\sim25 ps。

因此实际的时钟周期为:

Tcycle=Dn+S T_{\text{cycle}} = \frac{D}{n} + S

处理器的频率为f=1/Tcyclef = 1/T_{\text{cycle}}。使用流水线后的硬件面积为G+nLG + n \cdot L,其中LL为每级流水线寄存器的面积开销。

以Cost/Performance作为优化目标(即希望用最少的硬件面积获得最高的性能),其度量为:

CostPerf=面积×每条指令延迟=(G+nL)Tcycle=(G+nL)(Dn+S) \frac{\text{Cost}}{\text{Perf}} = \text{面积} \times \text{每条指令延迟} = (G + nL) \cdot T_{\text{cycle}} = (G + nL) \cdot \left(\frac{D}{n} + S\right)

展开得到:

CostPerf=GDn+GS+LD+LSn\frac{\text{Cost}}{\text{Perf}} = \frac{GD}{n} + GS + LD + LSn

其中GSGSLDLD是与nn无关的常数项。对nn求导并令其为零:

ddn(CostPerf)=GDn2+LS=0\frac{d}{dn}\left(\frac{\text{Cost}}{\text{Perf}}\right) = -\frac{GD}{n^2} + LS = 0

解得最优流水线级数(在纯Cost/Performance意义下):

nopt=GDLS \boxed{n_{\text{opt}} = \sqrt{\frac{GD}{LS}}}

这个公式的物理含义清晰:流水线的最优深度取决于"不使用流水线时的面积×\times延迟"与"每级寄存器的面积×\times延迟"之比的平方根。当组合逻辑的面积和延迟很大(GDGD大),而流水线寄存器的开销很小(LSLS小)时,应该使用更深的流水线。

在3 nm工艺下,代入典型参数:D3nsD \approx \SI{3}{ns}S20psS \approx \SI{20}{ps}G/L100G/L \approx 100(不使用流水线时的逻辑面积是每级寄存器面积的约100倍):

nopt=100×3ns20ps=15000122n_{\text{opt}} = \sqrt{\frac{100 \times \SI{3}{ns}}{\SI{20}{ps}}} = \sqrt{15000} \approx 122

这个结果远大于实际处理器的流水线深度(15\sim22级),说明纯Cost/Performance模型严重不足——它忽略了分支预测失败惩罚这一最重要的约束。

分支预测失败惩罚对最优深度的影响

在实际的超标量处理器中,分支预测失败是流水线深度的最大制约因素。当分支预测失败时,从分支指令的取指到其执行结果产生之间的所有流水段中的指令都需要被清除(flush),处理器需要从正确的地址重新开始取指。

设分支预测失败的惩罚(即需要清除的流水段数)为kk。在一个nn级流水线中,分支通常在执行阶段被解析,其位置大约在流水线的中部偏后,因此kk大约等于从取指到执行的级数,可以用kαnk \approx \alpha \cdot n近似,其中α0.60.8\alpha \approx 0.6\sim0.8

将分支预测失败的惩罚纳入性能模型。假设:

  • pbp_b:程序中分支指令的比例(典型值0.15\sim0.20,即每5\sim7条指令一条分支)

  • mm:分支预测失败率(典型值0.02\sim0.05,取决于预测器和工作负载)

  • k=αnk = \alpha \cdot n:每次预测失败的惩罚周期数

考虑分支预测失败后的有效CPI:

CPIeff=CPIbase+pbmk=CPIbase+pbmαn\mathrm{CPI}_{\text{eff}} = \mathrm{CPI}_{\text{base}} + p_b \cdot m \cdot k = \mathrm{CPI}_{\text{base}} + p_b \cdot m \cdot \alpha \cdot n

执行时间正比于:

TexecCPIeffTcycle=(CPIbase+pbmαn)(Dn+S) T_{\text{exec}} \propto \mathrm{CPI}_{\text{eff}} \cdot T_{\text{cycle}} = \left(\mathrm{CPI}_{\text{base}} + p_b m \alpha n\right) \cdot \left(\frac{D}{n} + S\right)

为简化分析,设CPIbase=1\mathrm{CPI}_{\text{base}} = 1,令β=pbmα\beta = p_b m \alpha,展开:

Texec=Dn+S+βD+βnSdTexecdn=Dn2+βS=0\begin{aligned} T_{\text{exec}} &= \frac{D}{n} + S + \beta D + \beta n S \\ \frac{d T_{\text{exec}}}{dn} &= -\frac{D}{n^2} + \beta S = 0 \end{aligned}

解得修正后的最优深度:

nopt=DβS=DpbmαS \boxed{n_{\text{opt}}' = \sqrt{\frac{D}{\beta S}} = \sqrt{\frac{D}{p_b \cdot m \cdot \alpha \cdot S}}}

注意与公式[eq:ch02-optimal-depth]的区别:面积因素G/LG/L被分支预测因素1/(pbmα)1/(p_b \cdot m \cdot \alpha)所替代。这说明在考虑分支预测失败后,最优深度不再取决于面积效率,而是取决于分支预测的精度。

代入典型参数:D=3nsD = \SI{3}{ns}pb=0.17p_b = 0.17m=0.03m = 0.03α=0.7\alpha = 0.7S=20psS = \SI{20}{ps}

β=0.17×0.03×0.7=3.57×103\beta = 0.17 \times 0.03 \times 0.7 = 3.57 \times 10^{-3}
nopt=3×1093.57×103×20×1012=3×1097.14×1014=42017205n_{\text{opt}}' = \sqrt{\frac{3 \times 10^{-9}}{3.57 \times 10^{-3} \times 20 \times 10^{-12}}} = \sqrt{\frac{3 \times 10^{-9}}{7.14 \times 10^{-14}}} = \sqrt{42017} \approx 205

这个值仍然偏大。这是因为我们的模型做了几个过于简化的假设:(a) CPIbase=1\mathrm{CPI}_{\text{base}} = 1忽略了Cache缺失、数据依赖等因素导致的额外CPI;(b) 假设分支预测失败惩罚是唯一的深度相关开销。更精确的模型需要加入以下因素:

  1. 流水线寄存器的功耗:每级流水线寄存器在每个周期都需要翻转,消耗可观的动态功耗。nn级流水线的寄存器功耗为Platch=nClatchV2fP_{\text{latch}} = n \cdot C_{\text{latch}} \cdot V^2 \cdot f。在20级流水线中,流水线寄存器可能占总功耗的10%\sim15%。如果将功耗作为约束条件(PtotalPbudgetP_{\text{total}} \leq P_{\text{budget}}),则最优深度会进一步降低。

  2. 时钟分配网络:更深的流水线对时钟信号的质量要求更高——时钟偏斜tskewt_{\text{skew}}需要更小,否则会侵蚀每级的有效逻辑延迟预算。更小的时钟偏斜需要更精细的时钟树设计(如H-tree或Fishbone结构),增加面积和功耗。

  3. 逻辑划分的不均匀性:理想模型假设电路延迟DD可以均匀地分配到nn级流水线的每一级(每级D/nD/n)。但实际上某些逻辑块无法在任意位置被流水线寄存器切割。例如一个组合逻辑乘法器的关键路径可能通过多个全加器级联,在某些全加器的中间插入寄存器会改变电路功能。这导致实际的各级延迟不均衡——最慢的一级决定了时钟周期,而其他级有空闲的时序余量。随着nn增大,这种不均衡越来越严重,频率提升的边际收益递减。

  4. 设计和验证复杂度:每增加一级流水线,就需要额外的旁路路径(用于不同延迟的FU之间的结果前递)、更多的流水线控制逻辑(stall、flush、bubble插入等)、以及更多的验证用例(corner case)。这增加了处理器的开发时间和成本,虽然不直接体现在公式中,但在工程实践中是重要的考量因素。

综合这些因素,现代高性能处理器的流水线深度在15\sim22级之间,这是在频率、IPC、功耗和设计复杂度之间取得的工程最优解。

图 2.6展示了考虑分支预测失败后,处理器性能(以1/Texec1/T_{\text{exec}}衡量)随流水线深度的变化关系。可以看到存在一个明确的性能峰值区间(约15\sim25级),超过这个区间后性能反而下降。

处理器性能随流水线深度的变化(归一化到20级时$m=2\%$的性能)。不同曲线对应不同的分支预测失败率$m$。蓝色阴影区域标示最优深度区间(15$\sim$22级)。
处理器性能随流水线深度的变化(归一化到20级时$m=2\%$的性能)。不同曲线对应不同的分支预测失败率$m$。蓝色阴影区域标示最优深度区间(15$\sim$22级)。

从图图 2.6可以得出以下关键观察:

  1. 在低预测失败率(m=2%m=2\%)下,性能峰值出现在n1722n \approx 17\sim22级,此时频率提升和分支惩罚增加达到平衡。继续增加深度到31级(Pentium 4的Prescott)会导致约10%的性能下降。

  2. 在中等预测失败率(m=5%m=5\%)下,性能峰值左移到n1517n \approx 15\sim17级,31级时性能下降约24%。这说明分支预测精度直接影响最优流水线深度。

  3. 在低预测精度(m=10%m=10\%,典型的2000年代早期预测器水平)下,超过15级的流水线几乎无法带来任何性能收益——频率提升完全被分支惩罚抵消。

这些曲线清楚地解释了为什么现代处理器(配备了m23%m \approx 2\sim3\%的高精度TAGE预测器)选择了15\sim22级的深度范围,以及为什么Pentium 4时代(预测器精度约m58%m \approx 5\sim8\%)的31级流水线是一个糟糕的选择。

过深流水线的教训:Pentium 4

Intel的Pentium 4处理器(代号Willamette/Prescott,基于NetBurst微架构,2000\sim2006年)是流水线深度过深的经典反面教材。NetBurst微架构的设计理念是:通过极深的流水线实现极高的时钟频率,从而在Performance=IPC×f\text{Performance} = \mathrm{IPC}\times f中靠频率ff的优势来弥补IPC的下降。

案例研究 4 — NetBurst微架构的流水线设计

Pentium 4的流水线深度经历了两代演进:

  • Willamette/Northwood(180 nm/130 nm,2000\sim2003年):20级流水线(Intel称为"Hyper-Pipeline"),频率范围1.5 GHz\sim3.4 GHz。

  • Prescott/Cedar Mill(90 nm/65 nm,2004\sim2006年):31级流水线,频率范围2.8 GHz\sim3.8 GHz。

31级流水线带来的严重问题:

(1) 分支预测失败惩罚巨大:从取指到分支执行结果产生约需20\sim30个周期(取决于指令类型和前端状态)。即使分支预测准确率达到95%(NetBurst时代的预测器远未达到今天TAGE级别的精度),每200条指令(约30条分支×\times5%失败率\approx1.5次失败)就要浪费1.5×2537.51.5 \times 25 \approx 37.5个周期。对于一个理想IPC为3的处理器,200条指令理想需要约67周期,分支惩罚增加37.5周期,等价于IPC从3.0下降到200/104.51.91200/104.5 \approx 1.91,性能损失约36%。

(2) 实际IPC极低:在SPEC CPU 2000上,Pentium 4 Prescott 3.6 GHz的单线程性能仅略高于Intel Pentium M 2.1 GHz(基于P6微架构,12\sim14级流水线)。换言之,Prescott需要70%更高的频率才能勉强匹配一个流水线深度仅为其一半的处理器。

(3) 功耗失控:为了维持3.6 GHz+的频率,Prescott的电压高达1.4 V。加上31级流水线寄存器的翻转功耗和更大的面积,Prescott在90 nm工艺下的TDP高达115 W。而同期的竞争对手AMD Athlon 64(K8微架构,12级流水线,90 nm)的TDP仅为89 W,单线程性能却更高。

(4) 频率目标未能实现:Intel原计划将NetBurst的频率推进到10 GHz以上(代号Tejas,计划使用65 nm工艺达到7 GHz+)。但由于功耗和散热问题(Prescott的功耗密度已经接近核反应堆堆芯),Intel不得不在2004年取消了4 GHz的Prescott产品计划,并最终取消了整个Tejas项目。

NetBurst的失败直接导致了Intel的架构转向:2006年推出的Core微架构(用于Core 2 Duo/Quad处理器)将流水线深度从31级缩减到14级,解码宽度维持在4-wide,大幅提高了IPC,同时将功耗降低到合理水平。Core微架构的设计理念——"适中的流水线深度,优先提高IPC,在功耗预算内适度提升频率"——成为后续所有Intel高性能处理器(Nehalem、Sandy Bridge、Haswell、直到Golden Cove/Lion Cove)的设计基础。

Pentium 4的教训可以总结为以下经验法则:

  1. 在现代分支预测器能力(预测失败率2%\sim5%)下,流水线深度超过22\sim25级后,频率提升带来的收益将被分支预测失败惩罚的增加所抵消。

  2. 频率的提升受限于功耗墙(power wall)——在给定的TDP预算下,频率不能无限提高。超过某个频率后,为了维持时序需要提高电压,而PV2fP \propto V^2 f使得功耗以超线性速度增长。

  3. 更深的流水线并不等于更高的性能。Performance=IPC×f\text{Performance} = \mathrm{IPC}\times f中的两个因素需要平衡。如果IPC下降的百分比大于频率提升的百分比,则总性能下降。

现代处理器的深度选择

吸取了Pentium 4的教训后,现代高性能处理器普遍将流水线深度控制在15\sim22级之间。不同处理器的具体深度取决于其目标频率、ISA特征和目标市场:

  • Apple Firestorm/Everest\sim16\sim17级):AArch64的固定长度编码使得解码阶段只需要1\sim2级(无需预解码和复杂的指令长度解析),因此整体流水线较浅。较浅的流水线意味着分支预测失败惩罚较小(约12\sim14个周期),但频率也较低(3.2 GHz\sim4.4 GHz)。Apple的策略是用更宽的设计来弥补较低的频率。

  • AMD Zen 4/Zen 5\sim19\sim20级):x86的变长指令需要预解码阶段来标记指令边界,增加了前端的级数。AMD的流水线深度比Apple深3\sim4级,但频率高出30%以上(5.7 GHz\sim5.8 GHz)。Zen系列采用了TAGE+感知机混合预测器来缓解深流水线带来的分支惩罚。

  • Intel Golden Cove/Lion Cove\sim20\sim22级):Intel的流水线最深,频率也最高(5.2 GHz\sim5.5 GHz)。Intel拥有业界最先进的工艺设计团队(尤其是高频电路设计能力),能够在更深的流水线下维持极高的频率。同时,Intel在分支预测器上的投入也是最大的——L1+L2 BTB共计超过20K项,TAGE预测器的历史长度可达数百位,占用的SRAM面积可达前端面积的30%以上。

  • ARM Cortex-X4\sim17级):与Apple类似,受益于AArch64的简单解码。ARM的Cortex-X系列定位于授权给第三方(如Qualcomm、MediaTek、Samsung)使用的高性能核心IP,需要在不同的工艺节点和SoC配置下都能良好工作,因此流水线深度选择了一个中间值。

  • 香山昆明湖\sim18级):RISC-V的32位固定指令格式使得解码简单,流水线深度与ARM Cortex-X4相当。目前实现在TSMC 14 nm工艺上(出于成本和可及性考虑),频率受限于工艺(约2.5 GHz)。未来如果迁移到先进工艺(5 nm以下),频率有望显著提升。

设计权衡 4 — 流水线深度与分支预测器投入的协同关系

流水线深度和分支预测器的复杂度之间存在着一种协同关系:更深的流水线需要更精确的分支预测器来缓解预测失败的惩罚;同时,更精确的分支预测器本身也需要更多的流水段来完成查询——TAGE预测器的多表查询通常需要2\sim3个周期,大容量BTB的查询也需要2\sim3个周期。这形成了一种正反馈循环:深流水线\to需要好预测器\to预测器自身需要多级\to流水线更深\to需要更好的预测器\to \ldots

不同厂商在这个循环中选择了不同的平衡点:

Intel策略:"投入大量硬件资源到分支预测器,承受较深的流水线,用高频换取性能"。Intel的分支预测器可能占据整个前端面积的30%以上,但由此实现的高预测准确率(>>97.5%)使得即使在20\sim22级流水线下,分支预测失败的IPC损失也控制在合理范围内。

Apple策略:"使用较浅的流水线(\sim17级),减少对高精度分支预测的依赖,将省下的面积预算用于更宽的执行引擎和更大的ROB/IQ"。Apple的分支预测器虽然也采用了TAGE架构,但由于流水线较浅,即使预测精度略低,惩罚也较小。

香山策略:作为开源RISC-V处理器,香山的设计兼顾了学术研究和工程实践。昆明湖采用了TAGE-SC预测器(约\sim18级流水线),在有限的面积预算下尽可能提高预测精度。

这三种策略各有优劣,适用于不同的市场需求。没有绝对的"最优深度"——最优解取决于目标频率、功耗预算、ISA、工艺节点和目标工作负载。

展望2030年代,流水线深度预计不会有大的变化——15\sim22级的范围已经被证明是非常稳健的工程选择。尽管未来的分支预测器会更加精确(可能将失败率降低到1%以下),但更精确的预测器本身也需要更多的流水段来完成查询,两者大致抵消。工艺进步(2 nm以下)带来的互连延迟相对增大(interconnect delay越来越dominant)可能使得实际的流水线级数略有增加,但总体趋势是稳定的。

真正可能改变流水线深度格局的是近阈值计算(Near-Threshold Computing, NTC)和异步流水线(Asynchronous Pipeline)等技术:

  • 近阈值计算通过降低供电电压到接近晶体管阈值的水平来大幅降低功耗,但代价是频率下降3\sim5倍。在这种情况下,更深的流水线(30+级)可能重新变得有吸引力,因为功耗预算的节省可以用来增加流水段数量和面积。

  • 异步流水线不使用全局时钟,每一级通过握手协议(handshake protocol)与相邻级通信,消除了时钟偏斜的限制。异步设计理论上可以实现更深的流水线而不受时钟分配网络的约束。但异步设计的EDA工具支持和设计方法学尚不成熟,在商用处理器中几乎没有使用。

这些前沿技术将在第第 54.0 章章中进一步探讨。

预测技术在超标量处理器中的角色

在深流水线超标量处理器中,"预测"是维持流水线高效运转的核心手段。处理器需要在各种信息尚未确定的情况下进行推测性执行(speculative execution),一旦预测正确,就避免了等待确定信息的延迟;如果预测错误,则需要回滚已经执行的推测性工作。可以说,没有预测技术,现代超标量处理器将无法运作——在一个20级流水线、8-wide的处理器中,如果每遇到一条分支指令就停下来等待其结果确定,处理器的有效IPC将不到1.0。

分支预测

分支预测是超标量处理器中最重要、也是最成熟的预测技术。它需要在取指阶段——分支指令甚至尚未被解码——就预测两个信息:分支方向(taken or not taken)和分支目标地址(target address)。

分支方向预测

现代处理器使用基于历史的预测器来预测分支方向。核心思想是:分支指令的行为具有时间局部性(temporal locality,一条分支过去的行为模式往往在未来重复)和关联性(correlation,一条分支的行为可能与之前执行的其他分支的行为相关)。

主流的方向预测器架构包括:

  • TAGE预测器(Tagged Geometric History Length Predictor):这是目前学术界和工业界最主流的预测器架构。TAGE使用多个表(通常4\sim8个),每个表使用不同长度的全局历史来索引。历史长度按几何级数递增(例如2、4、8、16、32、64、128位),使得TAGE可以同时捕捉短期模式和长期模式。在预测时,选择匹配的最长历史长度的表的预测结果。Intel Golden Cove、AMD Zen 4、ARM Cortex-X4以及香山昆明湖都使用了TAGE的变种。TAGE的预测准确率在SPEC CPU上可达97%\sim98%。

  • 感知机预测器(Perceptron Predictor):将每条分支视为一个线性二分类问题。为每条分支维护一组权重向量(与全局历史长度相同),预测结果为权重向量与历史位向量的点积的符号。感知机预测器的优势是可以学习到分支与非常远的历史位之间的关联性(传统预测器由于表大小限制难以使用很长的历史)。AMD Zen 2\simZen 5使用了TAGE+感知机的混合预测器,取两者的优势。

  • 统计校正器(Statistical Corrector,SC):在TAGE预测结果的基础上进行微调。SC维护一组辅助表,检测TAGE在某些特定模式下的系统性偏差,并在TAGE置信度低时翻转其预测。TAGE-SC-L(TAGE + SC + Loop Predictor)组合是目前分支预测锦标赛(Championship Branch Prediction,CBP)上的冠军级预测器。

  • 循环预测器(Loop Predictor):专门检测具有固定迭代次数的循环分支。通过记录循环的迭代计数来精确预测循环退出的时刻——这对于for循环等场景非常有效,而TAGE在这种场景下可能产生系统性的最后一次迭代误预测。

分支目标预测

分支方向预测只回答了"跳还是不跳"的问题,还需要预测"跳到哪里"。分支目标预测由以下硬件结构完成:

  • BTB(Branch Target Buffer):缓存最近执行过的分支指令的PC和目标地址。当取指地址命中BTB时,直接使用BTB中记录的目标地址。现代处理器的BTB容量为4K\sim16K项,通常分为多级(L1 BTB容量小但延迟低,L2 BTB容量大但延迟高)。Intel Golden Cove的L1 BTB约有6K\sim8K项,L2 BTB约有12K项。

  • RAS(Return Address Stack):专门用于函数返回指令(RET/JALR ra)的目标预测。RAS利用了函数调用和返回的嵌套配对关系:当遇到CALL指令时,将返回地址压入RAS;当遇到RET指令时,从RAS弹出目标地址。RAS的深度通常为16\sim48项,可以正确预测大多数函数返回的目标(除非递归深度超过RAS容量,或者RAS被投机性的错误路径指令污染)。

  • 间接分支预测器(Indirect Branch Predictor):对于目标地址非固定的间接分支(如C语言的switch-case编译后的跳转表、C++的虚函数调用),BTB只能记录最近一次的目标地址。如果间接分支在不同上下文中跳转到不同的目标,BTB的预测将频繁失败。更先进的间接分支预测器使用分支历史来索引一个目标地址表,以预测在不同执行上下文中的不同目标地址。

除了上述主要的预测器组件外,现代处理器还使用了一些辅助的预测机制来提高前端效率:

  • 取指目标队列(Fetch Target Queue,FTQ):位于分支预测器和I-Cache之间的缓冲队列,解耦分支预测和I-Cache访问。FTQ允许分支预测器以比I-Cache更快的速率产生预测结果(例如每周期预测2个分支),提前为I-Cache准备多个取指目标。当I-Cache因缺失而停顿时,FTQ中已有的预测结果可以避免重新查询分支预测器。典型的FTQ容量为16\sim32项。

  • uop缓存中的分支信息:微操作缓存中的每个uop通常会附带分支预测相关信息(如该uop是否是分支、预测方向等),使得从微操作缓存供给后端时不需要重新查询分支预测器。

  • 预解码分支标记:在I-Cache预解码阶段标记出分支指令的位置和类型(条件分支、无条件分支、函数调用、函数返回、间接分支),使得后续的BTB查询可以精确匹配。

分支预测的详细设计将在第第 13.0 章\sim第 17.0 章章中深入讨论,包括TAGE预测器的完整架构、BTB的组织方式、RAS的溢出和投机性恢复、以及间接分支预测器的设计。

值预测

值预测(Value Prediction,VP)是一种更加激进的推测技术,它试图预测指令执行的结果值本身。如果预测正确,依赖该指令结果的后续指令可以直接使用预测值开始执行,而不需要等待该指令实际完成——这等效于打破了RAW依赖链,暴露出更多的ILP。

值局部性

值预测的理论基础是值局部性(value locality):程序中很多指令的结果值具有可预测的模式。经典的研究(Lipasti, Wilkerson, Shen, 1996)表明,在SPEC CPU基准测试中,约50%\sim70%的指令的结果值等于该指令最近一次执行时产生的结果值。

常见的可预测模式包括:

  • 常值模式(Constant):一条load指令反复从同一个地址加载,其结果总是相同的值。典型场景包括全局配置变量、虚函数表指针等。

  • 步进模式(Stride):循环中的地址生成指令或循环计数器,其结果每次增加(或减少)一个固定的步长。例如:

    # 数组遍历 —— 指针每次增加8字节
        ld   x1, 0(x2)       # x2 的值: 0x1000, 0x1008, 0x1010, ...
        addi x2, x2, 8        # 步进模式, stride = 8
  • 上下文相关模式(Context-based):一条指令的结果值取决于之前的执行上下文。例如函数参数在不同调用点有不同的值,但在同一调用链中是可预测的。可以用类似TAGE的历史索引表来预测。

值预测器的硬件架构

硬件描述 3 — 值预测器的硬件架构

一个典型的值预测器包含以下核心组件:

  1. 值预测表(Value Prediction Table,VPT):以指令PC为索引,每个表项存储预测值(64位)、步进值(stride,64位)、预测模式标记(常值/步进/上下文)、以及置信度计数器(nn位饱和计数器,通常n=35n = 3\sim5位)。VPT的容量通常为1K\sim4K项。

  2. 置信度估计器(Confidence Estimator):跟踪每条指令的值预测准确率。只有当置信度超过阈值时才会使用预测值。值预测的代价远大于分支预测失败——错误的值预测可能导致整条依赖链上的多条指令需要重新执行——因此需要非常高的置信度(>>99%甚至>>99.5%)才敢启用预测。

  3. 验证逻辑:当指令实际执行完毕后,将实际结果与预测值比较。如果一致,增加置信度计数器;如果不一致,减少计数器并触发值预测失败恢复

  4. 恢复逻辑:值预测失败时,需要将所有使用了错误预测值的后续指令标记为"无效"并重新执行。恢复的范围可以从单条指令(如果预测值未被任何后续指令使用)到整条依赖链(如果预测值传播到了很长的依赖链中)。精确的选择性恢复(selective replay)需要跟踪值的传播关系,硬件实现非常复杂。简化的做法是在值预测失败时清除从该指令开始的所有后续指令(类似分支预测失败),但这会导致不必要的性能损失。

值预测的现状与前景

长期以来,值预测一直停留在学术研究阶段,没有商用处理器采用。原因在于:(a) 值预测失败的恢复机制复杂且代价高;(b) 在4-wide\sim6-wide的处理器中,传统的ILP挖掘手段(更大的ROB和IQ)已经足够,不需要如此激进的推测。

但近年来出现了值得注意的变化。Apple的M3/M4处理器被推测实现了某种形式的load值预测(Load Value Prediction,LVP)。尽管Apple没有公开确认这一点,但独立的性能分析师通过精心设计的微基准测试发现:在特定的load-dependent依赖链场景中,Apple M3/M4的性能表现无法用任何已知的传统微架构技术(如更低的Cache延迟、更大的ROB等)来解释,唯一合理的解释是处理器在推测性地预测load的返回值。如果这一推测成立,Apple将是第一个在商用处理器中大规模部署值预测的厂商。

值预测的潜在收益是巨大的。研究表明,在SPEC CPU基准测试中,理想的值预测器可以将IPC提升15%\sim25%(Perais and Seznec, 2014)。即使是保守的、只针对高置信度load指令的值预测器,也可以提升5%\sim10%的IPC。随着处理器宽度不断增大到8-wide乃至10-wide,传统的ILP挖掘手段面临边际收益递减的困境——ROB从512项增加到700项带来的IPC提升不到5%——值预测可能成为突破依赖链限制、进一步提升IPC的关键技术。

预测失败时的恢复

推测执行的代价是:预测错误时需要恢复处理器状态。恢复的速度直接影响处理器的性能——恢复越快,预测错误对性能的影响越小。现代处理器针对不同类型的预测失败,使用了不同的恢复机制,复杂度和恢复速度各不相同。

分支预测失败的恢复

分支预测失败的恢复是最常见的恢复场景。当一条分支指令在执行阶段确定预测错误后,处理器需要执行以下恢复操作:

  1. 清除错误路径指令:从ROB中标记或清除该分支指令之后的所有指令(这些指令位于错误的推测路径上),释放它们占用的所有资源(物理寄存器回收到Free List、IQ表项释放、SB/LQ表项清除等)。

  2. 恢复RAT:将Speculative RAT恢复为该分支指令执行时刻的正确映射状态。有两种主要方法:

    • 从ARAT恢复(Architecture RAT Recovery):将Architecture RAT(代表最近提交状态)复制到Speculative RAT,然后从ARAT对应的ROB位置开始,逐条重做ARAT和该分支之间所有已退休指令的重命名操作来重建正确的FRAT。这种方法恢复速度慢(需要遍历ROB中该分支之前的未退休指令),但硬件简单——不需要额外的存储来保存快照。

    • 从检查点恢复(Checkpoint Recovery):在每条分支指令分发时创建一个RAT快照(checkpoint)。预测失败时,直接从对应的检查点恢复RAT,速度非常快(1\sim2个周期)。代价是需要为每条飞行中的分支维护一个完整的RAT拷贝。Intel Golden Cove使用了这种方法,最多支持约48\sim64个同时飞行的分支快照。对于RISC-V的32个整数寄存器,每个快照需要32×9=28832 \times 9 = 288位,64个快照共需约2.25 KB的SRAM。

  3. 重定向取指:将前端的PC设置为分支的正确目标地址(如果预测的是not taken但实际是taken,则目标地址来自分支执行结果;如果预测的是taken但实际是not taken,则目标地址为分支指令的PC+4),从正确路径重新开始取指。

图 2.7展示了分支预测失败时的流水线行为。

分支预测失败时的流水线时序图。带阴影线的格子表示被清除的错误路径指令,绿色格子表示正确路径上新取的指令。从分支预测失败检测(周期8)到正确路径指令进入执行阶段约需8$\sim$10个周期。
分支预测失败时的流水线时序图。带阴影线的格子表示被清除的错误路径指令,绿色格子表示正确路径上新取的指令。从分支预测失败检测(周期8)到正确路径指令进入执行阶段约需8$\sim$10个周期。

从图图 2.7可以看到,分支预测失败的总惩罚可以分解为两部分:(1) 恢复延迟——从检测到预测失败到重定向取指开始,约1\sim2个周期(使用检查点恢复)或3\sim5个周期(使用ARAT恢复);(2) 流水线重填延迟——正确路径上的指令需要经历完整的取指-解码-重命名流程才能进入后端执行。总的分支预测失败惩罚约等于恢复延迟加上前端流水线深度。

存储器顺序违例的恢复

当一条load指令被推测为与之前的store指令不冲突而提前执行(乱序执行),但后来发现确实存在地址冲突(即store的地址计算完毕后发现与之前已执行的load地址相同),就发生了存储器顺序违例(memory order violation)。恢复方式取决于处理器的设计:

  • 完整清空(Full Pipeline Flush / Machine Clear):从违例的load指令开始清除所有后续指令,类似于分支预测失败的恢复。这种方式实现简单但代价大——很多不受违例影响的指令也被无辜清除了。Intel的早期处理器使用了这种方式。

  • 选择性重放(Selective Replay):只重新执行该load指令本身以及依赖该load结果的后续指令,不影响其他不相关的指令。这种方式性能更好但硬件实现复杂——需要跟踪指令之间的依赖关系并能够选择性地使某些指令的结果无效。现代处理器(如AMD Zen系列)越来越多地采用选择性重放。

为了减少存储器顺序违例的发生频率,现代处理器使用了Store Set预测器或类似的存储器消歧预测器:跟踪历史上哪些load-store指令对发生过地址冲突,当同样的指令对再次出现时,让load等待对应的store完成后再执行。这些内容将在第第 36.0 章章中详细讨论。

恢复延迟的优化

为了减少各种预测失败对性能的影响,现代处理器采用了多种优化手段来缩短恢复延迟:

  1. 提前分支解析(Early Branch Resolution):在执行阶段尽早计算分支结果。对于条件分支,如果源操作数已经通过旁路网络就绪,分支结果可以在执行的第一个周期就确定。部分处理器设置了专门的分支执行单元(Branch Unit),优先级最高、延迟为1个周期,以加速分支解析。AMD Zen系列的分支单元可以在ALU的第一级流水段就确定分支结果。

  2. 分支解析广播(Branch Resolution Broadcast):一旦分支结果确定,立即通过专用的高优先级信号通知前端(重定向取指PC)和ROB(标记是否需要清空)。这条广播路径绕过了正常的写回-唤醒流程,可以减少1\sim2个周期的恢复延迟。

  3. 前端解耦(Front-End Decoupling):使用足够大的指令队列(Fetch Queue / Instruction Buffer)将前端和后端解耦。在分支预测失败后,前端可以立即开始从正确路径取指,即使后端还在处理恢复操作(如清除ROB、恢复Free List等)。指令队列缓冲前端取出的正确路径指令,使得前端恢复和后端恢复可以重叠进行,减少了总的恢复延迟。

  4. 提前恢复RAT(Eager RAT Recovery):在使用检查点恢复方案的处理器中,一旦检测到分支预测失败,立即开始恢复FRAT(只需1\sim2个周期),而不需要等待ROB清空完成。FRAT恢复完成后,新的正确路径指令可以立即开始重命名和分发,与ROB的清空操作重叠进行。

性能分析 5 — 分支预测失败的性能影响量化

假设一个6-wide、20级流水线的处理器,分支预测失败惩罚为15个周期(含恢复延迟和流水线重填延迟),理想IPC=4.0\mathrm{IPC}= 4.0(考虑了数据依赖和Cache缺失后的估计值)。在SPEC INT工作负载中,约每6条指令有一条分支(pb0.167p_b \approx 0.167),预测失败率m=3%m = 3\%

每100条指令中约有16.7条分支,其中约0.5条预测失败(16.7×0.030.516.7 \times 0.03 \approx 0.5),每次预测失败浪费15个周期。

执行100条指令的理想周期数为100/4.0=25100/4.0 = 25个周期。分支预测失败增加0.5×15=7.50.5 \times 15 = 7.5个周期。实际总周期数约为25+7.5=32.525 + 7.5 = 32.5个周期。

实际IPC:100/32.53.08100 / 32.5 \approx 3.08

分支预测失败导致的IPC下降:

IPCidealIPCactualIPCideal=4.03.084.023%\frac{\mathrm{IPC}_{\text{ideal}} - \mathrm{IPC}_{\text{actual}}}{\mathrm{IPC}_{\text{ideal}}} = \frac{4.0 - 3.08}{4.0} \approx 23\%

这个数字说明,即使在97%的预测准确率下,分支预测失败仍然是IPC的重要限制因素。

灵敏度分析:如果预测准确率提高到99%(失败率降为1%),分支预测失败增加的周期数降为0.167×15=2.50.167 \times 15 = 2.5,IPC变为100/27.53.64100/27.5 \approx 3.64,下降约9%。这说明预测准确率每提高1个百分点(从97%到98%到99%),都能带来约7%的IPC收益——这正是为什么处理器厂商在分支预测器上投入如此大量的硬件资源。

其他形式的预测与推测

除了分支预测、值预测和存储器消歧推测外,现代处理器还使用了其他形式的预测技术:

  • Load延迟预测(Load Latency Prediction):预测一条load指令是否会在L1D Cache中命中。如果预测命中,则进行推测唤醒(如前文所述);如果预测缺失,则延迟唤醒依赖指令,避免无用的重放。Load延迟预测器通常基于load指令的PC和最近的Cache命中/缺失历史。

  • 路预测(Way Prediction):在组相联Cache中,预测目标数据位于哪一路,从而只读取该路的数据而非所有路。这可以减少Cache访问的功耗(只激活一个SRAM Bank而非所有Bank),但如果预测错误需要额外一个周期重新读取正确的路。路预测在功耗敏感的移动处理器中使用较多。

  • 预取预测(Prefetch Prediction):预测程序未来可能访问的数据地址,提前将数据从低层Cache/内存加载到L1/L2 Cache中。预取预测器包括流预取器(Stream Prefetcher)、步进预取器(Stride Prefetcher)和更复杂的时间关联预取器(Temporal Prefetcher)。这些内容将在第第 9.0 章\sim第 10.0 章章中详细讨论。

  • Cache行预测(Line Fill Prediction / Next-Line Prediction):预测I-Cache缺失时L2 Cache返回的数据,以减少前端停顿。这在Intel的某些处理器中有使用。

可以说,现代超标量处理器是一台推测执行机器——它的大部分工作都是在各种预测的基础上推测性地进行的。只有在指令退休时,推测性的结果才会被确认为正确的架构状态。这种"先做后验"的设计范式是超标量处理器高性能的根基,同时也是其设计复杂性的主要来源。

表 2.6总结了现代超标量处理器中使用的主要预测技术及其特征。

预测类型预测对象典型精度失败代价详见章节
分支方向taken/not taken97–99%15–20周期第 13.0 章第 16.0 章
分支目标目标PC95–99%15–20周期第 17.0 章
存储器消歧load-store冲突98–99.5%10–20周期第 36.0 章
值预测指令结果值99–99.5%+10–25周期本节
Load延迟L1命中/缺失90–95%5–10周期第 30.0 章
路预测Cache路编号90–95%1–2周期第 5.0 章
预取未来访问地址60–90%带宽浪费第 9.0 章第 10.0 章

现代超标量处理器中的主要预测技术总结

从表表 2.6可以看出,不同类型的预测在精度和失败代价上存在显著差异。分支预测的失败代价最高(15\sim20周期的流水线清空),因此对精度的要求也最高(97%以上)。路预测的失败代价很低(仅1\sim2周期),因此可以容忍较低的精度(90%即可)。值预测的失败代价虽然也较高,但通过高置信度阈值(只在>>99.5%准确率时才启用),可以将实际失败率控制在极低水平。

设计提示

本章详细介绍了现代超标量处理器流水线的完整结构,从取指到提交的每一个阶段,以及流水线宽度和深度这两个基本参数的设计权衡。关键要点如下:

  1. 现代OoO处理器的流水线包含15\sim22级,按功能分为前端(分支预测、取指、预解码、解码)、中端(重命名、分发)和后端(发射、执行、写回、提交)三大部分。前端是顺序的,后端是乱序的,提交阶段恢复为顺序——这是"顺序取指、乱序执行、顺序退休"的经典OoO范式。

  2. 流水线宽度的增加带来超线性的硬件复杂度增长(特别是RAT、旁路网络和IQ)和边际收益递减的IPC提升(受限于程序中可用的ILP)。当前高性能核心的宽度为6\sim10-wide,预计到2030年可能进一步增加到12-wide。

  3. 流水线深度受限于分支预测失败惩罚、流水线寄存器功耗和设计复杂度。Pentium 4(31级)的教训表明过深的流水线在IPC×f\mathrm{IPC}\times f的总体性能上得不偿失。当前高性能核心的深度为15\sim22级,这是频率、IPC和功耗之间的工程最优平衡点。

  4. 预测技术(分支预测、值预测等)是维持深流水线高效运转的关键。分支预测准确率每提高1%可带来约7%的IPC收益。值预测有望成为未来10-wide+处理器进一步提升IPC的突破性技术。

  5. 预测失败的恢复机制(从ARAT恢复 vs. 检查点恢复)直接影响恢复速度和面积开销。现代处理器倾向于使用检查点恢复以实现最快的恢复速度。

后续章节将深入每一个子系统的详细设计:第第 5.0 章\sim第 12.0 章章讲述Cache与存储层次,第第 13.0 章\sim第 17.0 章章讲述分支预测,第第 18.0 章\sim第 23.0 章章讲述指令集与解码,第第 24.0 章\sim第 39.0 章章讲述乱序执行引擎的各个组成部分。

在超标量流水线中,数据冒险检测是硬件正确性的基础。下面的SystemVerilog代码展示了一个简化的RAW(Read After Write)冒险检测比较器,用于判断两条相邻指令之间是否存在源/目的寄存器冲突。在nn-wide超标量处理器中,需要O(n2)O(n^2)个这样的比较器来检查所有指令对之间的冒险——这正是宽度增加带来超线性硬件复杂度增长的一个具体例子。

verilog
module raw_hazard_detector #(
    parameter REG_ADDR_W = 6  // 寄存器编号位宽(64个逻辑寄存器)
)(
    // 前一条指令(生产者)的目的寄存器
    input  logic                  prod_rd_valid,  // 是否写目的寄存器
    input  logic [REG_ADDR_W-1:0] prod_rd,        // 目的寄存器编号

    // 后一条指令(消费者)的两个源寄存器
    input  logic                  cons_rs1_valid,  // 是否读rs1
    input  logic [REG_ADDR_W-1:0] cons_rs1,        // 源寄存器1编号
    input  logic                  cons_rs2_valid,  // 是否读rs2
    input  logic [REG_ADDR_W-1:0] cons_rs2,        // 源寄存器2编号

    // 冒险检测结果
    output logic                  hazard_rs1,   // rs1存在RAW冒险
    output logic                  hazard_rs2,   // rs2存在RAW冒险
    output logic                  hazard_any    // 存在任意RAW冒险
);
    // RAW: 消费者的源 == 生产者的目的, 且双方valid, 且不是x0
    assign hazard_rs1 = prod_rd_valid & cons_rs1_valid
                      & (prod_rd == cons_rs1)
                      & (prod_rd != {REG_ADDR_W{1'b0}});  // 排除x0

    assign hazard_rs2 = prod_rd_valid & cons_rs2_valid
                      & (prod_rd == cons_rs2)
                      & (prod_rd != {REG_ADDR_W{1'b0}});

    assign hazard_any = hazard_rs1 | hazard_rs2;
endmodule

在一个6-wide处理器中,取指组中的6条指令需要(62)=15\binom{6}{2} = 15对这样的比较器来检测所有可能的RAW冒险。实际硬件还需要处理WAR和WAW冒险(由寄存器重命名解决)以及跨取指组的冒险(由记分板或发射队列解决),但上述比较器展示了冒险检测的核心逻辑。

设计提示

前向桥接。本章分析了流水线宽度和深度的数学模型,但这些模型中的许多参数——关键路径延迟、SRAM访问时间、旁路网络的可达范围——都取决于底层半导体工艺的物理约束。第 3.0 章将从晶体管结构、导线RC延迟、功耗密度等物理基础出发,揭示微架构设计的"物理边界"在哪里:为什么bypass网络不能无限长?为什么Cache不能无限大?为什么频率不能无限高?这些物理约束将为本章讨论的流水线深度和宽度选择提供最终的工程解释。

正文与图片:CC BY-NC-SA 4.0 · 本仓库少量站点配置代码:MIT