Skip to content

Cache的基本原理

硬件描述 1 — 历史时刻:从属存储器的诞生

1965年,Maurice Wilkes在IBM Systems Journal上发表了一篇仅3页的论文,描述了一种"从属存储器"(slave memory)的概念——在处理器和主存之间放置一个小而快的缓冲区,自动保存最近访问过的数据。这个简单的想法从根本上改变了计算机体系结构的面貌。六十年后的今天,一颗现代处理器中的Cache面积占据了芯片总面积的50%\sim70%。Cache是处理器设计中单一最重要的结构,没有之一。

设计提示

统一视角审视:Cache是对数据访问局部性的投机(speculation)——处理器投机地假设最近访问过的数据在不久的将来还会被再次访问。这种投机的成功率(即命中率)直接决定了处理器能否达到其理论性能上限。回顾第 1.0 章中的CPI公式,CPIcache\text{CPI}_{\text{cache}}分量——由Cache缺失引入的每指令额外周期数——往往是制约实际IPC最主要的因素。同时,Cache的物理实现受到第 3.0 章中讨论的SRAM单元面积、漏电功耗和互联延迟等工艺约束的直接限制:晶体管预算决定了Cache能有多大,SRAM访问延迟决定了Cache能有多快,而功耗墙决定了Cache能同时激活多少路。

处理器的性能在很大程度上取决于存储系统能否及时地向执行单元供给指令和数据。一个6-wide乱序超标量处理器在峰值状态下每周期需要从指令Cache读取6条指令、从数据Cache发起2\sim3次访存操作,而主存(DRAM)的访问延迟却高达数十纳秒——在5 GHz的处理器上,这意味着200\sim300个时钟周期的等待。如果处理器每次都直接访问主存,那么即使拥有再宽的发射宽度和再深的乱序窗口,也无法发挥出应有的性能。

Cache(高速缓冲存储器)正是为了弥合处理器与主存之间巨大的速度鸿沟而引入的关键硬件机制。自1960年代IBM System/360 Model 85首次在商用计算机中引入Cache以来,Cache已成为所有现代处理器中不可或缺的核心组成部分。本章将系统地阐述Cache的基本原理,包括局部性原理、Cache的组织方式、写入策略和替换策略。这些内容构成了后续章节讨论高级Cache优化技术的基础。

局部性原理

Cache之所以能有效工作,根本原因在于程序对存储器的访问模式具有局部性(Locality)。局部性原理是计算机体系结构中最重要的经验法则之一:如果程序访问了某个存储器地址,那么在不远的将来,程序很可能再次访问这个地址或其附近的地址。局部性分为时间局部性和空间局部性两种形式。

时间局部性

时间局部性(Temporal Locality)是指:如果一个数据项在某个时刻被访问,那么在不久的将来它很可能再次被访问。时间局部性广泛存在于各种程序行为中:

(1)循环中的指令。程序中的循环体会被反复执行,循环体中的每条指令在每次迭代中都会被重新取指和执行。一个执行1000次的循环,其循环体中的指令将在短时间内被访问1000次。对于高性能处理器而言,这意味着循环体中的指令一旦被取入L1 I-Cache,在整个循环执行过程中几乎都可以在L1中命中,极大地减少了取指的开销。

(2)频繁访问的变量。循环计数器、累加器等变量在循环的每次迭代中都会被读取和写入。例如以下C代码中的变量sum

c
int sum = 0;
for (int i = 0; i < N; i++)
    sum += a[i];  // sum 在每次迭代中被读写

变量sum在整个循环过程中被连续读写NN次,具有极强的时间局部性。在处理器内部,sum会被分配到物理寄存器中(由寄存器重命名机制完成),甚至不需要真正访问L1 D-Cache。但对于那些无法被寄存器分配捕获的频繁访问变量(如数组中反复访问的热点元素),Cache的时间局部性捕获就至关重要。

(3)栈帧中的数据。函数的局部变量存储在栈帧中,在函数执行期间会被反复访问。由于函数调用通常不会太深(除递归外),栈的活跃区域通常只有几KB到几十KB,具有极强的时间局部性。在SPEC CPU 2017等基准测试中,栈数据的L1 D-Cache命中率通常高达99%以上。

(4)全局热点数据。操作系统的任务控制块(TCB)、进程页表、文件系统的inode等数据结构在系统运行期间会被频繁访问。数据库系统中的索引节点、Web服务器中的路由表等应用级数据结构也具有类似的时间局部性。

空间局部性

空间局部性(Spatial Locality)是指:如果一个数据项被访问,那么与它地址相近的数据项很可能在不久的将来也会被访问。空间局部性同样普遍存在于各种程序行为中:

(1)顺序执行的指令。程序中大部分指令是按顺序执行的(分支指令的占比通常为15%\sim25%,且其中约60%\sim70%是前向分支,即taken的概率较低)。因此,如果处理器取了地址AA处的指令,那么地址A+4A+4(RISC-V和ARM的32位指令)处的指令很可能马上就会被需要。在64 B的Cache行中可以存放64/4=1664 / 4 = 16条RISC-V指令,这意味着一次I-Cache缺失之后的连续15条指令都可以在同一Cache行内命中。

(2)数组的遍历。对数组的顺序遍历是最经典的空间局部性场景。当程序访问a[i]时,a[i+1]a[i+2]等后续元素很快就会被访问。由于数组元素在内存中连续存放,它们通常位于同一个Cache行中。以int类型数组为例(每个元素4字节),一个64 B的Cache行可以容纳64/4=1664/4=16个元素,因此一次Cache缺失可以为后续15次访问提供数据。

(3)结构体字段的访问。当程序访问一个结构体的某个字段时,通常也会访问该结构体的其他字段。由于结构体字段在内存中连续排列,这些访问具有良好的空间局部性。例如:

c
struct Point { double x, y, z; };
// 访问 p->x 后通常很快会访问 p->y 和 p->z
double dist = sqrt(p->x*p->x + p->y*p->y + p->z*p->z);

Point结构体共24字节,完全可以放在一个64 B的Cache行内,三个字段的访问只需要一次Cache缺失。

(4)栈的增长。函数调用时,新的栈帧在栈顶分配,其地址与调用者的栈帧相邻。因此连续的函数调用和返回会形成对栈空间的局部性访问。

Cache的设计正是利用了这两种局部性:时间局部性决定了一旦某个数据被访问就应当将其保留在Cache中;空间局部性决定了Cache应当以Cache行(cache line)而非单个字节为基本传输单位——当一个字节被访问时,将其所在的整个Cache行(通常为64字节)一并取入Cache,从而使得同一行中其他尚未被访问的字节也可以在后续的访问中直接从Cache获得。

关于Cache行大小的选择:更大的Cache行可以更好地利用空间局部性(每次缺失取入更多的相邻数据),但也带来两个问题——一是传输时间更长(从下级存储取回一个128 B的Cache行所需时间约为64 B的两倍),二是Cache中的有效数据密度可能降低(如果程序只访问Cache行中的少部分字节)。在实际测量中,64 B是一个在各种工作负载下表现良好的折中点,因此几乎所有现代通用处理器都采用64 B的Cache行大小。部分面向高带宽应用的处理器(如某些GPU和向量处理器)使用128 B或256 B的Cache行。

存储器层次结构

基于局部性原理,现代计算机系统构建了一个多级的存储器层次结构(Memory Hierarchy):越靠近处理器的存储层越快、越小、越贵;越远离处理器的存储层越慢、越大、越便宜。每一层都作为下一层的Cache,利用局部性来捕获大部分访问,使得绝大多数存储操作可以在较快的层中完成。

图 5.1展示了典型的存储器层次结构金字塔图。

存储器层次结构金字塔
存储器层次结构金字塔

表 5.1详细列出了2025年前后主流高性能处理器各存储层次的典型参数。这些参数来自公开的微架构分析和厂商文档,不同处理器之间会有差异,但数量级是一致的。

从表表 5.1中可以观察到几个重要的特征:

(1)延迟的巨大差距。L1 Cache到DRAM的延迟差距达到了50\sim100倍(3\sim5周期 vs. 150\sim300周期)。如果没有Cache层次结构,处理器每次访存都需要等待上百个周期,这将使IPC降低到远低于1的水平。

(2)容量与延迟的此消彼长。更大的Cache意味着更高的命中率(因为可以容纳更多的工作集),但也意味着更长的访问延迟。这是因为更大的SRAM阵列需要更长的字线(wordline)和位线(bitline),信号传播延迟随阵列面积的平方根增长。例如,L1 D-Cache通常限制在32\sim80 KB以保持3\sim5周期的延迟;如果将L1扩大到256 KB,其延迟可能增加到8\sim10个周期,反而会降低整体性能,因为加载延迟的增加会拉长关键依赖链。

(3)带宽与层次的关系。L1 Cache的带宽远高于L2和L3。这是因为L1 Cache通常有多个读写端口(如2个读端口 + 1个写端口),每周期可以并行服务多个加载/存储操作;而L2和L3通常只有较少的端口(或使用分时复用),带宽低于L1。

表 5.2列出了几款典型处理器的详细Cache参数。

从表表 5.2中可以看到几个有趣的设计选择。Apple的处理器采用了极大的L1 Cache(L1I 192 KB,L1D 128 KB),其延迟仍然控制在3个周期以内,这得益于TSMC 3 nm工艺的小晶体管尺寸和苹果先进的物理设计能力。Intel Golden Cove的L1D选择了48 KB/12-way这个看似奇怪的配置,这是为了满足VIPT约束(将在第 6.0 章中讨论)。AMD Zen 4则保持了传统的32 KB/8-way配置,以在面积和延迟之间取得平衡。

存储器层次结构的核心设计目标可以用一个简单的公式来概括。设hih_i为第ii层Cache的命中率,tit_i为其命中时的访问延迟,则平均访存延迟(Average Memory Access Time, AMAT)为:

AMAT=t1+(1h1)[t2+(1h2)[t3+(1h3)tmem]] \mathrm{AMAT} = t_1 + (1 - h_1)\bigl[t_2 + (1 - h_2)\bigl[t_3 + (1 - h_3) \cdot t_{\mathrm{mem}}\bigr]\bigr]

其中tmemt_{\mathrm{mem}}为主存的访问延迟。

性能分析 1 — AMAT计算实例

假设以下参数:

  • L1:命中率95%,延迟4周期

  • L2:命中率(在L1缺失中)80%,延迟12周期

  • L3:命中率(在L2缺失中)90%,延迟40周期

  • 主存:延迟200周期

则AMAT为:

AMAT=4+0.05×[12+0.20×(40+0.10×200)]=4+0.05×[12+0.20×60]=4+0.05×24=5.2 周期\begin{aligned} \mathrm{AMAT} &= 4 + 0.05 \times [12 + 0.20 \times (40 + 0.10 \times 200)] \\ &= 4 + 0.05 \times [12 + 0.20 \times 60] \\ &= 4 + 0.05 \times 24 \\ &= 5.2 \text{ 周期} \end{aligned}

即通过三级Cache层次,平均访存延迟仅为5.2个周期,相比直接访问主存的200个周期降低了97.4%。这就是存储器层次结构的威力。

如果L1命中率从95%提高到97%,AMAT将从5.2周期降低到4+0.03×24=4.724 + 0.03 \times 24 = 4.72周期。这说明L1命中率每提高1%,AMAT就能降低约0.24个周期——在5 GHz的处理器上,这可能带来显著的IPC提升。

缺失可以按照原因分为三类(即经典的3C模型):

  • 强制缺失(Compulsory Miss):也称冷启动缺失(Cold-Start Miss),是指第一次访问某个Cache行时必然发生的缺失,因为Cache中尚未包含该行的数据。强制缺失与Cache的容量和相联度无关。

  • 容量缺失(Capacity Miss):程序的工作集超过了Cache的容量,导致有用的Cache行被驱逐,后续再次访问时发生缺失。增大Cache容量可以减少容量缺失。

  • 冲突缺失(Conflict Miss):在非全相联的Cache中,多个地址映射到同一组,即使Cache中还有空闲位置(在其他组中),也可能因为目标组已满而发生缺失。增加相联度可以减少冲突缺失。

在多核系统中,还有第四种缺失——一致性缺失(Coherence Miss),也称共享缺失(Sharing Miss):由于另一个核修改了当前核Cache中的数据,导致当前核的Cache行被无效化(Invalidated),后续再次访问时发生缺失。这将在第 11.0 章中讨论。

图 5.2展示了3C模型中三类缺失随Cache容量和相联度变化的趋势。在Cache容量较小时,容量缺失占主导地位;随着容量增大,容量缺失减少,但冲突缺失可能仍然显著;增大相联度可以有效减少冲突缺失,但对容量缺失无帮助。

3C模型:缺失率随Cache容量和相联度的变化趋势(示意图)
3C模型:缺失率随Cache容量和相联度的变化趋势(示意图)

从图图 5.2中可以观察到:(1)随着Cache容量增大,所有配置的缺失率都在下降,但下降速度逐渐减缓——这体现了工作集大小的分布特征,大部分程序的核心工作集集中在几十KB到几MB的范围内。(2)从直接映射到8-way,冲突缺失显著减少;但从8-way到全相联,差距已经很小,说明8-way组相联已经消除了绝大部分冲突缺失。(3)强制缺失是所有缺失的下界,与Cache配置无关,只能通过预取(Prefetching)来减少。

Cache的组成方式

Cache的核心问题是:给定一个由处理器产生的内存地址,如何快速地判断这个地址对应的数据是否在Cache中(即是否命中),以及如果命中,如何快速地将数据取出。Cache的组成方式(或称映射方式)决定了地址到Cache位置的映射关系,直接影响命中率、访问延迟和硬件复杂度。

在讨论Cache的组成方式之前,需要深入理解Cache的物理实现基础——SRAM(Static Random-Access Memory,静态随机存取存储器)。Cache的Tag和Data存储阵列都使用SRAM实现。与DRAM(Dynamic RAM)不同,SRAM不需要周期性刷新,读写速度快(可以在一两个时钟周期内完成),但面积较大。理解SRAM的位单元结构和阵列组织对于理解Cache的延迟、功耗和面积特性至关重要。

6T SRAM位单元

SRAM的基本存储单元是6T位单元(6-transistor bit cell),由6个MOS晶体管组成。图图 5.3展示了6T SRAM位单元的晶体管级电路结构。

6T SRAM位单元的晶体管级电路结构
6T SRAM位单元的晶体管级电路结构

6T位单元的核心是两个交叉耦合的CMOS反相器。左侧反相器由PMOS管P1和NMOS管N1组成,右侧反相器由P2和N2组成。两个反相器的输出分别连接到对方的输入,形成一个双稳态锁存器(Bistable Latch)——电路有两个稳定状态:Q=1/Q\overline{Q}=0和Q=0/Q\overline{Q}=1,分别代表存储的值为1和0。只要电源持续供电,锁存器就会保持当前状态不变。

两个NMOS存取晶体管AXL和AXR充当开关的角色,它们的栅极连接到字线(Word Line, WL)。当WL为高电平时,存取晶体管导通,将内部存储节点Q和Q\overline{Q}分别连接到位线BL和BL\overline{\text{BL}},使得外部电路可以读取或写入位单元的内容。当WL为低电平时,存取晶体管截止,位单元与外部隔离,存储的数据不受影响。

6T位单元的读操作

读操作的过程如下:

  1. 位线预充电。在读操作开始前,将BL和BL\overline{\text{BL}}都预充电到VDDV_{DD}(或VDD/2V_{DD}/2,取决于设计)。预充电通常由一个专用的预充电电路完成,耗时约为半个时钟周期。

  2. 字线使能。将目标行的WL拉高,存取晶体管AXL和AXR导通。

  3. 电荷分享与放电。假设位单元存储的值为Q=0、Q\overline{Q}=1。此时N1导通(栅极为Q\overline{Q}=1)、P1截止,左侧节点Q被拉低。通过导通的AXL,BL上的预充电荷开始向Q节点(即通过N1到地)放电,BL的电压缓慢下降。而在右侧,P2导通(栅极为Q=0),Q\overline{Q}节点维持在VDDV_{DD}BL\overline{\text{BL}}的电压几乎不变。

  4. 灵敏放大器检测。经过一定的延迟后,BL和BL\overline{\text{BL}}之间出现一个微小的电压差ΔV\Delta V(通常为50 mV\sim200 mV)。灵敏放大器(Sense Amplifier, SA)检测到这个差分电压,并将其放大为完整的逻辑电平(0或VDDV_{DD}),作为读出数据输出。

读操作的一个关键设计约束是读稳定性(Read Stability):当BL通过AXL对Q节点放电时,Q节点的电压会被短暂地抬高(因为BL的预充电荷流入Q节点),这可能导致N2的栅极电压升高,使N2开始导通,从而改变存储状态——这就是所谓的读扰动(Read Disturb)。为了防止读扰动,需要精心设计存取管和下拉管的尺寸比:下拉管N1的导通电阻必须远小于存取管AXL的导通电阻,即N1的宽长比(W/L)N1(W/L)_{N1}必须大于AXL的宽长比(W/L)AXL(W/L)_{AXL}。通常要求βratio=(W/L)N1/(W/L)AXL>1.5\beta_{\text{ratio}} = (W/L)_{N1} / (W/L)_{AXL} > 1.5

这个约束直接影响了6T位单元的面积:不能随意缩小下拉管的尺寸,否则读稳定性会受到损害。这也是6T SRAM面积无法像DRAM那样激进缩减的原因之一。

6T位单元的写操作

写操作的过程如下:

  1. 驱动位线。写驱动电路(Write Driver)将要写入的数据驱动到位线上:如果写入1,则BL=VDDV_{DD}BL\overline{\text{BL}}=0;如果写入0,则BL=0、BL\overline{\text{BL}}=VDDV_{DD}

  2. 字线使能。将WL拉高,导通存取管。

  3. 翻转存储节点。假设当前Q=0、Q\overline{Q}=1,要写入1(Q\leftarrow1、Q\overline{Q}\leftarrow0)。写驱动电路通过BL\overline{\text{BL}}(=0)和导通的AXR将Q\overline{Q}节点的电压强制拉低。当Q\overline{Q}降到N1的阈值电压以下时,N1关断、P1导通,Q节点被拉高到VDDV_{DD},完成状态翻转。

写操作的关键设计约束是写裕度(Write Margin):存取管必须能够"战胜"上拉管,将存储节点的电压从VDDV_{DD}拉低到翻转阈值以下。这要求存取管AXR的导通能力大于上拉管P2的导通能力,即(W/L)AXR>(W/L)P2(W/L)_{AXR} > (W/L)_{P2}

读稳定性和写裕度对存取管的尺寸要求是矛盾的:读稳定性要求存取管较弱(以免干扰存储节点),而写裕度要求存取管较强(以便翻转存储节点)。6T位单元的设计就是在这两个矛盾约束之间取得平衡。在先进工艺节点下,由于晶体管特性的变异(Process Variation)增大,这种平衡变得更加困难。

下面的SystemVerilog代码以行为级描述了SRAM读操作的四个时序阶段——预充电、字线使能、灵敏放大、数据输出——展示了第 3.0 章讨论的SRAM访问延迟如何分解为物理上不同的操作步骤。在实际的Cache流水线中,这些步骤通常被分配到不同的流水段中(如第 6.0 章将讨论的L1D多周期流水线)。

verilog
module cache_sram_read_ctrl #(
    parameter ROWS     = 256,   // 行数(Cache的组数)
    parameter COLS     = 512,   // 列数(每行的位数)
    parameter ADDR_W   = $clog2(ROWS)  // 行地址位宽
)(
    input  logic                clk,
    input  logic                rst_n,
    input  logic                rd_req,       // 读请求
    input  logic [ADDR_W-1:0]   rd_addr,      // 行地址(Index)
    output logic [COLS-1:0]     rd_data,      // 读出数据
    output logic                rd_valid,     // 数据有效

    // SRAM阵列接口(简化)
    output logic                precharge_en, // 预充电使能
    output logic                wl_en,        // 字线使能
    output logic [ADDR_W-1:0]   wl_addr,      // 字线地址
    output logic                sa_en,        // 灵敏放大器使能
    input  logic [COLS-1:0]     sa_out        // 灵敏放大器输出
);
    // 读操作四阶段状态机
    typedef enum logic [1:0] {
        S_IDLE      = 2'b00,  // 空闲:等待请求
        S_PRECHARGE = 2'b01,  // 阶段1:位线预充电
        S_WORDLINE  = 2'b10,  // 阶段2:字线使能,电荷分享
        S_SENSE     = 2'b11   // 阶段3:灵敏放大+输出锁存
    } state_t;

    state_t state, next_state;
    logic [ADDR_W-1:0] addr_reg;

    always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) state <= S_IDLE;
        else        state <= next_state;

    always_comb begin
        next_state   = state;
        precharge_en = 1'b0;
        wl_en        = 1'b0;
        wl_addr      = addr_reg;
        sa_en        = 1'b0;
        rd_valid     = 1'b0;
        rd_data      = '0;

        case (state)
            S_IDLE: begin
                if (rd_req) next_state = S_PRECHARGE;
            end
            S_PRECHARGE: begin
                precharge_en = 1'b1;  // BL, BLB -> Vdd
                next_state   = S_WORDLINE;
            end
            S_WORDLINE: begin
                wl_en = 1'b1;         // 选中行, 电荷分享
                next_state = S_SENSE;
            end
            S_SENSE: begin
                sa_en    = 1'b1;      // 放大微小差分电压
                rd_data  = sa_out;    // 锁存输出
                rd_valid = 1'b1;      // 阶段4: 数据有效
                next_state = S_IDLE;
            end
        endcase
    end

    always_ff @(posedge clk)
        if (rd_req && state == S_IDLE)
            addr_reg <= rd_addr;
endmodule

8T SRAM位单元

8T SRAM位单元通过增加2个晶体管来解决6T位单元的读扰动问题,同时提供一个独立的读端口。图图 5.4展示了8T位单元的结构。

8T SRAM位单元:6T核心 + 2T独立读端口
8T SRAM位单元:6T核心 + 2T独立读端口

8T位单元在6T核心的基础上增加了两个NMOS晶体管RD1和RD2,构成一个独立的读端口

  • RD1的栅极连接到内部存储节点Q\overline{Q},漏极连接到RD2的源极,源极连接到地。

  • RD2的栅极连接到读字线RWL(Read Word Line),漏极连接到读位线RBL(Read Bit Line)。

  • RD1和RD2串联形成一个条件放电路径:只有当RWL=1(选中该行)Q\overline{Q}=1(存储值为0)时,RBL才会通过RD2\toRD1\toGND放电。如果Q\overline{Q}=0(存储值为1),RD1截止,RBL保持预充电的高电平。

8T位单元的核心优势是读操作不再干扰存储节点:读操作通过独立的RBL进行,存取管WAL_L和WAR_R在读操作期间保持关断(WWL=0),存储节点Q和Q\overline{Q}与外部完全隔离。这彻底消除了6T位单元的读扰动问题,允许更激进的晶体管尺寸缩减,并且可以在更低的电源电压下可靠工作。

8T位单元的代价是面积增加约30%\sim40%(因为增加了2个晶体管和额外的读位线、读字线布线),以及读端口只支持单端读取(只有RBL,没有差分RBL\overline{\text{RBL}}),这要求读灵敏放大器具有更高的灵敏度。在先进工艺节点(5 nm及以下),由于晶体管变异性增大、工作电压降低,8T位单元在低功耗设计中越来越受到青睐。部分移动处理器的L1 Cache已经开始采用8T或更先进的位单元设计。

在实际的SRAM设计中,还存在一些介于6T和8T之间的变体。例如,6T+位单元通过在标准6T结构中调整晶体管的阈值电压(使用高阈值管作为存取管以减少漏电流,同时使用低阈值管作为下拉管以提高读电流),在不增加晶体管数量的前提下改善读稳定性。10T位单元则在8T的基础上增加第二个独立的读端口(再加2个晶体管),提供2读1写共3个端口,适用于需要双读端口的L1 Cache Tag SRAM。

位单元类型的选择通常由工艺团队(Foundry)和处理器设计团队共同决定。每个工艺节点的PDK(Process Design Kit)通常会提供多种SRAM位单元选项,包括高密度(HD,最小面积)、高性能(HP,最快速度)和低功耗(LP,最低漏电)三种优化方向。L1 Cache通常使用HP型位单元以获得最低的访问延迟,而L3 Cache可能使用HD型位单元以最大化容量。

以TSMC的N5(5 nm)工艺为例,其SRAM位单元选项包括:HD型面积约0.021 μm2(用于大容量L3/LLC),HP型面积约0.026 μm2(用于高速L1/L2),LP型面积约0.024 μm2但采用高阈值晶体管以降低静态功耗(用于移动处理器的L2/L3)。在更先进的N3E(3 nm增强版)工艺中,HD型位单元面积进一步缩小到约0.016 μm2。不同位单元类型的读写速度差异可达20%\sim30%,设计团队需要根据目标Cache层级的延迟和功耗要求选择合适的位单元类型。在同一个处理器芯片上,不同层级的Cache可以使用不同类型的位单元——这是现代SoC设计中常见的做法。

SRAM阵列的物理组织

单个SRAM位单元只能存储1位数据。为了构成一个完整的Cache存储阵列,需要将大量的位单元组织成行和列的二维阵列,并配备解码器、灵敏放大器、写驱动器等外围电路。图图 5.5展示了一个典型SRAM阵列的结构。

SRAM阵列的基本组织结构
SRAM阵列的基本组织结构

SRAM阵列的访问过程可以分解为以下步骤,每一步都有对应的延迟开销:

  1. 行解码(Row Decode):将行地址翻译为字线选择信号。RR行的解码器需要log2R\lceil\log_2 R\rceil位地址输入,通过log2R\lceil\log_2 R\rceil级逻辑门产生RR条字线中的一条使能信号。解码器延迟约为O(logR)O(\log R)级门延迟。对于256行的SRAM(如32KB/8-way Cache的一路Data SRAM),行解码器约需3\sim4级门延迟。

  2. 字线传播:字线信号从解码器驱动到阵列中每一列的位单元。字线的电阻和电容随阵列宽度线性增长,传播延迟随阵列列数增大。

  3. 位线放电:被选中行的位单元通过存取管将存储电荷分享到位线上,在BL和BL\overline{\text{BL}}之间产生差分电压。位线的电容随阵列行数线性增长(因为每一行的位单元都连接到同一对位线),行数越多,位线电容越大,放电越慢。

  4. 灵敏放大(Sense Amplification):灵敏放大器检测到位线上的微小差分电压后,将其放大为完整的数字逻辑电平。灵敏放大器本身的延迟约为1\sim2个FO4门延迟。

  5. 列选择和输出驱动:如果阵列的列数大于需要的输出位宽,列MUX根据列地址从多列中选择需要的数据位。

更大的SRAM阵列有更长的字线和位线,因此访问延迟更长。为了控制大容量SRAM的延迟,通常将阵列分成多个子阵列(sub-array),通过分级的解码和选择来平衡延迟和面积。例如,一个256KB的SRAM可以分成16个16KB的子阵列,每个子阵列的字线和位线长度只有整体阵列的1/41/4,延迟大幅降低。代价是需要额外的子阵列选择逻辑和数据路由布线。

Cache中的Tag SRAM和Data SRAM

在一个组相联Cache中,Tag和Data分别存储在独立的SRAM阵列中。以一个64 KB、4-way、64 B Cache行的Cache为例,两个SRAM的组织方式如下:

Data SRAM

  • 总数据量:64×1024×8=52428864 \times 1024 \times 8 = 524288位(不含ECC)

  • 每路容量:64KB/4=16KB=13107264\,\text{KB} / 4 = 16\,\text{KB} = 131072

  • 组数:64KB/(64B×4)=25664\,\text{KB} / (64\,\text{B} \times 4) = 256

  • 每行存储宽度:64B=51264\,\text{B} = 512

  • 阵列组织:每路为一个256×512256 \times 512的SRAM阵列,共4路。行地址为8位Index,列输出为512位的完整Cache行数据。

Tag SRAM

  • 假设48位物理地址,Offset = log264=6\log_2 64 = 6位,Index = log2256=8\log_2 256 = 8位,Tag = 4886=3448 - 8 - 6 = 34位。

  • 每项包含:34位Tag + 1位Valid + 1位Dirty + 2位一致性状态 = 38位。

  • 阵列组织:256×(38×4)256 \times (38 \times 4)的SRAM阵列(所有4路的Tag存储在同一行中),或4个独立的256×38256 \times 38阵列。

  • 总Tag SRAM容量:256×38×4=38912256 \times 38 \times 4 = 389124.75KB\approx 4.75\,\text{KB}

在先进工艺节点(如5 nm和3 nm)下,一个SRAM 6T位单元的面积约为0.021 μm2\sim0.026 μm2。一个32 KB的SRAM阵列(32×1024×8=26214432 \times 1024 \times 8 = 262144位)的纯位单元面积约为262144×0.0246300μm20.0063mm2262144 \times 0.024 \approx 6300\,\mu\text{m}^2 \approx 0.0063\,\text{mm}^2,加上外围解码和读写电路后约为0.01 mm2\sim0.02 mm2。这只是一个核心面积(通常5 mm2\sim15 mm2)的很小一部分。然而,L2和L3 Cache的面积要大得多——一个2 MB的L2 Cache可能占据0.5 mm2以上,而一个32 MB的L3 Cache可能占据30 mm2以上,往往成为芯片面积的主要组成部分。

表 5.3对比了不同SRAM位单元类型的关键特性。

位单元类型晶体管数面积读端口写端口读稳定性
6T标准6\sim0.024  μm21(共享)1(共享)一般
8T读分离8\sim0.034  μm21(独立)1优秀
10T双端口10\sim0.055  μm221优秀

SRAM位单元类型比较(5 nm工艺)

SRAM访问的延迟分解

理解SRAM阵列的访问延迟对于设计高性能Cache至关重要。一次SRAM读操作的延迟可以分解为以下几个组成部分:

TSRAM=Tdecode+Twordline+Tbitline+TSA+Toutput T_{\text{SRAM}} = T_{\text{decode}} + T_{\text{wordline}} + T_{\text{bitline}} + T_{\text{SA}} + T_{\text{output}}

其中:

  • TdecodeT_{\text{decode}}为行解码器延迟。对于RR行的阵列,使用log2R\lceil \log_2 R \rceil级NAND/NOR门的树形解码器,延迟约为log2R×tgate\lceil \log_2 R \rceil \times t_{\text{gate}}。对于256行,Tdecode8×10ps=80psT_{\text{decode}} \approx 8 \times 10\,\text{ps} = 80\,\text{ps}

  • TwordlineT_{\text{wordline}}为字线传播延迟,取决于字线的RC延迟(电阻×\times电容)。字线跨越整个阵列宽度,其延迟约为12RC\frac{1}{2}RC,其中RR为字线总电阻,CC为字线总电容。对于512列的阵列,Twordline30ps50psT_{\text{wordline}} \approx 30\,\text{ps}\sim50\,\text{ps}

  • TbitlineT_{\text{bitline}}为位线差分放电延迟,取决于位线电容和位单元的驱动能力。位线电容随阵列行数线性增长。对于256行的阵列,Tbitline50ps100psT_{\text{bitline}} \approx 50\,\text{ps}\sim100\,\text{ps},是整个访问延迟中最大的组成部分。

  • TSAT_{\text{SA}}为灵敏放大器检测延迟,约20 ps\sim40 ps。

  • ToutputT_{\text{output}}为输出驱动和布线延迟,约10 ps\sim30 ps。

以一个32 KB、8-way的L1 D-Cache Data SRAM为例(每路64×51264 \times 512位阵列,64行,512列),典型的读取延迟约为:

TSRAM60+30+60+30+20=200ps1个周期(@5GHz)T_{\text{SRAM}} \approx 60 + 30 + 60 + 30 + 20 = 200\,\text{ps} \approx 1\text{个周期}(@5\,\text{GHz})

加上行解码器和Way MUX的延迟,总的Cache访问延迟约为2\sim3个周期。这与表表 5.2中列出的实际处理器L1 D-Cache延迟一致。

硬件描述 2 — SRAM子阵列划分的实践

为了控制大容量SRAM阵列的访问延迟,通常采用分层子阵列(Hierarchical Sub-array)结构。以一个1 MB的L2 Cache Data SRAM为例:

  • 如果使用单一阵列:8192×10248192 \times 1024位,位线有8192行的负载,字线跨越1024列。位线延迟将超过500 ps,根本无法满足高频设计的要求。

  • 分成64个16 KB子阵列:每个子阵列128×1024128 \times 1024位。位线只有128行负载,延迟大幅降低。子阵列之间通过全局位线和局部灵敏放大器级联,增加的全局布线延迟约为50 ps\sim100 ps,远小于减少的位线延迟。

  • 子阵列的选择通过Index的高位实现:高6位(a13a8a_{13}\cdots a_8)选择64个子阵列之一,低7位(a7a1a_7\cdots a_1)作为子阵列内部的行地址。

这种分层设计使得大容量SRAM的延迟近似以O(C)O(\sqrt{C})而非O(C)O(C)增长,是控制L2/L3 Cache延迟的关键技术。

为了将上述SRAM访问的时序分解具象化,下面给出一个简化的SRAM读操作时序控制逻辑的SystemVerilog实现。该模块按照式 (5.2)中的五个阶段,用有限状态机驱动解码、字线使能、位线预充电/感应放大和输出锁存等信号,帮助读者建立SRAM控制逻辑的硬件直觉。

verilog
// 简化的 SRAM 读操作时序控制逻辑
// 对应公式 T_SRAM = T_decode + T_wordline + T_bitline + T_SA + T_output
module sram_read_ctrl #(
    parameter ADDR_W = 8,    // 行地址宽度 (256 rows)
    parameter DATA_W = 512   // 数据宽度 (64B cache line)
)(
    input  logic                clk, rst_n,
    input  logic                rd_req,        // 读请求
    input  logic [ADDR_W-1:0]   row_addr,      // 行地址
    output logic                decode_en,     // 行解码器使能
    output logic                wl_en,         // 字线驱动使能
    output logic                bl_precharge,  // 位线预充电
    output logic                sa_en,         // 灵敏放大器使能
    output logic                out_latch,     // 输出锁存
    output logic                rd_valid       // 数据有效
);

    // SRAM 读时序的五个阶段 (简化为单周期状态机)
    typedef enum logic [2:0] {
        IDLE    = 3'd0,  // 空闲
        DECODE  = 3'd1,  // T_decode: 行地址解码
        WL_DRIVE= 3'd2,  // T_wordline: 字线驱动传播
        BL_SENSE= 3'd3,  // T_bitline + T_SA: 位线放电与感应放大
        OUTPUT  = 3'd4   // T_output: 输出驱动与锁存
    } state_t;

    state_t state, next_state;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) state <= IDLE;
        else        state <= next_state;
    end

    always_comb begin
        next_state  = state;
        decode_en   = 1'b0;
        wl_en       = 1'b0;
        bl_precharge= 1'b0;
        sa_en       = 1'b0;
        out_latch   = 1'b0;
        rd_valid    = 1'b0;

        case (state)
            IDLE: begin
                bl_precharge = 1'b1;  // 空闲时保持位线预充电
                if (rd_req) next_state = DECODE;
            end
            DECODE: begin
                decode_en = 1'b1;     // 激活行解码器
                next_state = WL_DRIVE;
            end
            WL_DRIVE: begin
                wl_en = 1'b1;         // 字线使能, 位单元开始放电位线
                next_state = BL_SENSE;
            end
            BL_SENSE: begin
                sa_en = 1'b1;         // 灵敏放大器检测差分信号
                next_state = OUTPUT;
            end
            OUTPUT: begin
                out_latch = 1'b1;     // 锁存输出数据
                rd_valid  = 1'b1;     // 数据有效
                next_state = IDLE;
            end
            default: next_state = IDLE;
        endcase
    end
endmodule

直接映射

直接映射(Direct-Mapped)Cache是最简单的Cache组织方式:内存中的每个地址块只能映射到Cache中唯一确定的一个位置。具体来说,设Cache共有SS个Cache行(set),Cache行大小为BB字节,则内存地址AA映射到Cache的第A/BmodSA / B \bmod S行。

为了实现这种映射,需要将内存地址划分为三个字段:Tag(标记)、Index(索引)和Offset(块内偏移),如图图 5.6所示。

Cache地址的三字段划分
Cache地址的三字段划分
  • Offsetbb位):用于选择Cache行中具体的字节。若Cache行大小为BB字节,则b=log2Bb = \log_2 B。对于64 B的Cache行,b=6b = 6

  • Indexss位):用于选择Cache中的某一行(在组相联中称为某一组)。若Cache共有SS个行/组,则s=log2Ss = \log_2 S

  • Tagtt位):地址的高位部分,存储在Tag SRAM中,用于在命中判断时与访问地址的高位进行比较。t=nsbt = n - s - b,其中nn为地址总位数。

图 5.7展示了直接映射Cache的硬件结构。当处理器发出一个访存请求时,Cache控制器将地址的Index字段送入Tag SRAM和Data SRAM,同时读出该行的Tag值和数据。然后将读出的Tag值与地址的Tag字段进行比较,同时检查该行的Valid位是否为1。如果Tag匹配且Valid为1,则表示命中(hit),将数据从Data SRAM中取出,通过Offset字段选择需要的字节送回处理器;否则为缺失(miss),需要从下一级存储中取回数据。

直接映射Cache的硬件结构
直接映射Cache的硬件结构

直接映射Cache的优点是结构简单、速度快——只需要一个比较器,且Tag SRAM和Data SRAM可以并行访问(因为不需要先确定路的选择)。其主要缺点是冲突缺失问题:如果两个频繁访问的地址恰好映射到同一个Cache行,它们会不断地互相驱逐,即使Cache中还有大量空闲的行。

例如,在一个32 KB的直接映射Cache中(512个行,行大小64 B),地址0x0000_10000x0000_9000具有相同的Index(它们相差正好32 KB),因此它们总是竞争同一个Cache行。如果程序交替访问这两个地址,Cache将在每次访问时都发生缺失,命中率为0%。这就是所谓的乒乓效应(Ping-Pong Effect)。

在某些病态的访问模式下(如以特定步长遍历数组),直接映射Cache的命中率可能急剧下降。考虑以下代码:

c
// 假设 a 和 b 的基地址在 Cache 中映射到相同的组
for (int i = 0; i < N; i++)
    c[i] = a[i] + b[i];  // a[i] 和 b[i] 互相驱逐

如果数组ab的大小恰好是Cache容量的整数倍,那么a[i]b[i]会映射到相同的Cache行,导致每次访问都缺失。这种情况在科学计算中尤为常见,是直接映射Cache被淘汰出高性能处理器的主要原因。

考虑一个1 KB的直接映射Cache,行大小16 B,共64行,32位地址。地址划分为:Tag 22位、Index 6位、Offset 4位。

假设处理器依次访问以下地址序列:

0x0000_00000x0000_00100x0000_00000x0000_04000x0000_0000

访问过程:

  1. 0x0000_0000:Tag=0x00000,Index=0,Offset=0。Cache初始为空(Valid=0),缺失。从内存取回行填入Index 0,设Valid=1,存Tag。

  2. 0x0000_0010:Tag=0x00000,Index=1,Offset=0。Index 1的Valid=0,缺失。取回行填入Index 1。

  3. 0x0000_0000:Tag=0x00000,Index=0。Index 0已有Valid=1且Tag匹配,命中

  4. 0x0000_0400:Tag=0x00000,Index=0(0x400/16mod64=64mod64=0\text{0x400} / 16 \bmod 64 = 64 \bmod 64 = 0),但Tag=0x00001。Index 0的Tag不匹配,冲突缺失。旧行被替换。

  5. 0x0000_0000:Tag=0x00000,Index=0。Index 0的Tag为0x00001,不匹配,又一次冲突缺失

第4、5次访问展示了典型的冲突缺失:两个地址映射到同一Index但Tag不同,互相驱逐。

在早期的处理器设计中(如MIPS R2000/R3000,1985\sim1988年),由于晶体管预算有限,直接映射Cache凭借其简单的结构和低延迟而得到广泛使用。但随着晶体管预算的增长和对性能要求的提高,组相联Cache逐渐成为主流。到2000年代中期,所有主流高性能处理器都已采用至少4-way组相联的L1 Cache。

组相联

为了缓解直接映射Cache的冲突问题,可以增加每个Index位置可以存放的Cache行的数量。在组相联(Set-Associative)Cache中,Cache被划分为若干个(Set),每个组中包含WW个Cache行(也称为WW路,way)。内存中的每个地址块可以映射到其对应组中的任意一路。WW-way组相联Cache可以容忍最多WW个映射到同一组的地址块同时存在于Cache中,从而大大降低了冲突缺失的概率。

对于WW-way组相联Cache,若总容量为CC字节,Cache行大小为BB字节,则:

  • 总Cache行数 = C/BC / B

  • 组数 S=C/(B×W)S = C / (B \times W)

  • Index位数 s=log2S=log2Clog2Blog2Ws = \log_2 S = \log_2 C - \log_2 B - \log_2 W

  • Offset位数 b=log2Bb = \log_2 B

  • Tag位数 t=nsbt = n - s - b

注意:在容量CC和行大小BB固定的情况下,增加相联度WW会减少组数SS,从而减少Index的位数ss,相应地增加Tag的位数tt。Tag位数的增加意味着每个Tag项占用更多的空间,再乘以路数WW的增加,Tag SRAM的总容量会显著增大。

现代高性能处理器的L1 D-Cache普遍采用8-way到12-way的相联度:Intel Golden Cove的L1D为48 KB/12-way,AMD Zen 4的L1D为32 KB/8-way,Apple M4的P-core L1D为128 KB/8-way。更高的相联度可以进一步降低冲突缺失,但会增加硬件复杂度和访问延迟(更多路的Tag需要并行比较,更宽的Way MUX增加了关键路径延迟)。

组相联Cache的访问有两种主要的硬件实现方式:并行访问(Parallel Access)和串行访问(Serial Access)。

方式一:并行访问Tag和Data

在并行访问方式中,Tag SRAM和Data SRAM同时被访问:Index字段同时索引所有路的Tag SRAM和Data SRAM,读出所有路的Tag值和数据。然后将每一路读出的Tag与地址的Tag字段进行比较,确定哪一路命中,最后通过一个WW:1的MUX从所有路的数据中选出命中路的数据。图图 5.8展示了这一过程。

并行访问Tag和Data的4-way组相联Cache
并行访问Tag和Data的4-way组相联Cache

并行访问方式的优点是延迟低——Tag比较和Data读取同时进行,最终只需要一个MUX延迟来选择命中路的数据。其总延迟约为:

Tparallel=TSRAM read+TMUXT_{\text{parallel}} = T_{\text{SRAM read}} + T_{\text{MUX}}

其中Tag比较延迟被SRAM读取延迟所掩盖(比较器在SRAM读出之后即可工作,MUX在比较结果出来之后选择数据)。在5 nm工艺下,一个32 KB/8-way Cache的并行访问延迟约为3\sim4个周期(包括SRAM读取、Tag比较和MUX选择)。

但并行访问方式的缺点是功耗较高:无论最终哪一路命中,所有WW路的Data SRAM都被完整地读出了。Data SRAM的读取功耗远大于Tag SRAM(因为Data SRAM每项的位宽是整个Cache行,如64 B = 512位,而Tag只有20\sim36位),因此并行访问方式中,约有(W1)/W(W-1)/W比例的Data SRAM读取能量被浪费了。对于8-way Cache,这意味着87.5%的Data SRAM读取能量是浪费的。在移动和嵌入式处理器中,这是一个严重的功耗负担。

方式二:串行访问Tag然后Data

在串行访问方式中,首先只访问Tag SRAM,完成命中判断并确定命中的路号,然后仅读取命中路的Data SRAM。这种方式的功耗低(只读取一路的Data SRAM),但延迟更长:

Tserial=TTag SRAM read+Tcompare+TData SRAM readT_{\text{serial}} = T_{\text{Tag SRAM read}} + T_{\text{compare}} + T_{\text{Data SRAM read}}

具体来说,串行访问的第一步是用Index读取所有路的Tag并与地址的Tag字段比较,确定命中路号;第二步是用Index和命中路号去选择对应路的Data SRAM进行读取。由于第二步依赖第一步的结果,两者无法重叠,总延迟是两步之和。

串行访问方式通常用于L2/L3等对延迟要求不那么严格但对功耗敏感的Cache层。对于L1 D-Cache这种延迟极为关键的结构(因为加载延迟直接影响关键数据依赖链的长度),大多数高性能处理器采用并行访问方式,但通过Way Prediction等技术来降低功耗(见5.2.8 节)。

组相联Cache访问的完整步骤追踪

为了更好地理解组相联Cache的工作过程,下面以一个32 KB/4-way/64 B行大小的Cache为例,详细追踪一次Load操作的完整硬件步骤。

Cache参数:32 KB/4-way,64 B行大小,48位物理地址。

  • 总行数 = 32768/64=51232768 / 64 = 512行,组数 = 512/4=128512/4 = 128

  • Offset = 6位(a5a0a_5 \cdots a_0),Index = 7位(a12a6a_{12} \cdots a_6),Tag = 35位(a47a13a_{47} \cdots a_{13}

假设处理器执行一条Load指令:LD x5, 0x0000_3A7C_0158(从地址0x3A7C_0158加载8字节到寄存器x5)。

第一步:地址解析。

字段位范围
完整地址a47a_{47}a0a_00x0000_3A7C_0158
Taga47a_{47}a13a_{13}0x0000_1D3E_0 = 0x0001D3E0的高35位
Indexa12a_{12}a6a_6(0x1586)AND0x7F=5AND0x7F=5(0x158 \gg 6)\,\text{AND}\,0x7F = 5\,\text{AND}\,0x7F = 5
Offseta5a_5a0a_00x158AND0x3F=0x18=240x158\,\text{AND}\,0x3F = 0x18 = 24

Index = 5,表示访问Cache的第5组。Offset = 24,表示从Cache行的第24字节开始读取8字节(双字加载)。

第二步:并行读取Tag和Data SRAM。 用Index = 5同时索引4路的Tag SRAM和Data SRAM:

  • Tag SRAM读出第5组的4个Tag值:Tag0_0, Tag1_1, Tag2_2, Tag3_3。假设读出的值分别为:

    Way 0Way 1Way 2Way 3
    Tag0x73F100x1D3E00x55AA00x00000
    Valid1110
  • Data SRAM同时读出第5组的4路数据(每路64 B)。

第三步:Tag比较。 将地址的Tag字段(0x1D3E0)与4路读出的Tag值逐一比较:

  • Way 0:0x73F10 \neq 0x1D3E0不匹配

  • Way 1:0x1D3E0 == 0x1D3E0 且 Valid = 1,命中!

  • Way 2:0x55AA0 \neq 0x1D3E0,不匹配

  • Way 3:Valid = 0,无论Tag值如何都不匹配

命中向量 = 0100(Way 1命中)。

第四步:Way MUX选择。 4:1 Way MUX根据命中向量0100选择Way 1的64 B数据输出。

第五步:数据对齐。 从Way 1的64 B数据中,根据Offset = 24提取第24\sim31字节(共8字节),作为Load的结果送回寄存器文件。

第六步:更新替换状态。 将Way 1标记为MRU。如果使用Tree-PLRU,更新从根到Way 1的路径上的位。如果使用年龄矩阵,设第1行为全1、第1列为全0。

上述步骤中的第二步和第三步是延迟的主要来源。在并行访问方式下,第二步(SRAM读取)和第三步的Tag比较部分是并行进行的,总延迟约为TSRAM+TMUXT_{\text{SRAM}} + T_{\text{MUX}}。在串行方式下,第三步的Data SRAM读取需要等待Tag比较完成后才能开始,总延迟增加了一个Tag SRAM读取和比较的时间。

硬件描述 3 — 命中检测的硬件实现

Tag比较器的硬件实现需要关注以下细节:

  • 比较器宽度:每个比较器的位宽等于Tag位数(如35位)。使用35个XNOR门逐位比较,再用一个35输入的AND门汇总结果。35输入的AND门需要log235=6\lceil\log_2 35\rceil = 6级逻辑门(使用树形AND结构),延迟约为6×8ps=48ps6 \times 8\,\text{ps} = 48\,\text{ps}

  • Valid位检查:比较结果需要与Valid位进行AND运算。只有Tag匹配Valid = 1时,该路才算命中。这只需要一个额外的AND门。

  • 命中信号生成:4路的命中信号hit0,hit1,hit2,hit3\text{hit}_0, \text{hit}_1, \text{hit}_2, \text{hit}_3应为独热码(one-hot)——正常情况下恰好有一个为1(命中)或全为0(缺失)。如果多个位为1(多路同时匹配),则表示Cache状态出错——硬件通常会对此进行校验并触发机器检查异常。

  • 全局命中/缺失信号HIT=hit0hit1hit2hit3\text{HIT} = \text{hit}_0 \lor \text{hit}_1 \lor \text{hit}_2 \lor \text{hit}_3。这个信号决定了后续操作——HIT时将数据送回处理器,MISS时启动缺失处理流程。

总的Tag比较延迟约为:XNOR延迟 + 树形AND延迟 + Valid AND延迟 \approx 10 ps + 48 ps + 8 ps == 66 ps,约为1个FO4门延迟的3\sim4倍。这个延迟与SRAM读取延迟相比很短,因此在并行访问方式下,Tag比较几乎完全被SRAM读取延迟所掩盖。

表 5.4展示了在32 KB L1 D-Cache上,相联度对缺失率的影响(基于SPEC CPU 2017的模拟数据)。

相联度平均缺失率相对直接映射的改善Tag SRAM开销
直接映射(1-way)3.8%1.09 KB(512×17512 \times 17位)
2-way2.9%23.7%-23.7\%1.13 KB(512×18512 \times 18位)
4-way2.3%39.5%-39.5\%1.19 KB(512×19512 \times 19位)
8-way2.1%44.7%-44.7\%1.25 KB(512×20512 \times 20位)
16-way2.0%47.4%-47.4\%1.31 KB(512×21512 \times 21位)
全相联1.9%50.0%-50.0\%需要CAM

相联度对32 KB L1 D-Cache缺失率的影响(SPEC CPU 2017平均值)

从表表 5.4中可以看到两个重要规律:

第一,从直接映射到4-way的改善最为显著(缺失率降低了近40%),这说明大部分冲突缺失可以通过中等程度的相联度来消除。从4-way到8-way的额外改善(5%左右)虽然较小,但在高性能处理器中仍然是值得的。

第二,Tag SRAM的开销增长非常缓慢(每增加一倍相联度,Tag只增加1位),但Data SRAM的组织和MUX的宽度增长才是更大的硬件代价。一个8-way Cache需要8路的Data SRAM同时输出并通过8:1 MUX选择,这对布局布线和时序都是较大的挑战。

从8-way到16-way的改善已经非常微小(仅约0.1个百分点的缺失率),因此除了为满足VIPT约束外,通常不会在L1 Cache中使用16-way以上的相联度。

设计权衡 1 — 并行访问与串行访问的权衡

表 5.5总结了两种访问方式的对比。在实际设计中,L1 Cache几乎总是采用并行访问(以满足严格的延迟约束),而L2/L3 Cache可以根据设计目标选择串行访问或并行访问。一些处理器(如ARM的Cortex-A系列)在L1 Cache中采用"半并行"方式:先并行读取所有路的Tag,但只读取预测命中路的Data,如果预测错误则在下一周期重新读取正确路的Data。这种方式在大多数情况下获得与并行访问相同的延迟,但功耗接近串行访问。

特性并行访问串行访问
延迟低(1个SRAM延迟 + MUX)高(2个SRAM延迟 + 比较)
功耗高(读取所有WW路的Data)低(只读取命中路的Data)
Data SRAM端口数需要WW个读端口1个读端口即可
适用场景L1 CacheL2/L3 Cache
硬件复杂度中等(需要宽MUX)较低

全相联

全相联(Fully Associative)Cache是组相联Cache的极端情况:只有一个组(S=1S = 1),所有的Cache行都在同一个组中,因此内存中的任何地址块都可以放置在Cache的任意一个位置。全相联Cache的地址中没有Index字段,只有Tag和Offset。

全相联Cache的命中判断需要将访问地址的Tag与Cache中所有行的Tag同时进行比较。这种并行比较通常使用CAM(Content-Addressable Memory,内容寻址存储器)来实现。CAM是一种特殊的存储器,它不是通过地址来访问数据,而是通过内容(搜索关键字)来进行并行匹配。

图 5.9展示了CAM的基本结构。CAM中的每一项包含一个存储单元和一个比较电路。当搜索关键字输入时,所有项同时将自身存储的内容与搜索关键字进行比较,匹配的项输出高电平,通过一个优先编码器确定匹配的位置。

全相联Cache的CAM+SRAM结构
全相联Cache的CAM+SRAM结构

全相联Cache的优点是完全消除了冲突缺失——只要Cache还有空闲行,任何新的地址块都可以放入。其缺点是:

  • CAM面积大、功耗高。CAM每一位都包含一个存储单元和一个比较电路,其面积约为普通SRAM的3\sim4倍,功耗也高得多。在先进工艺下,一个CAM位单元的面积约为SRAM位单元的2.5\sim4倍。

  • 可扩展性差。CAM的搜索延迟和功耗随项数线性增长。对于几十项的CAM,延迟尚可接受(约1个周期);对于数百甚至数千项的CAM,延迟和功耗都无法承受。

  • 匹配线的电容问题。每条匹配线(Match Line)需要连接所有位的比较结果,其电容随位宽和项数增加而增大,导致匹配判断的速度降低。

因此,全相联结构通常只用于项数较少(通常不超过64\sim128项)的场合:

  • TLB(Translation Lookaside Buffer):存储页表项的Cache,L1 DTLB通常只有32\sim96项,采用全相联结构。全相联TLB可以最大化页表项的利用率,避免因冲突而频繁进行页表遍历(Page Table Walk)。

  • Victim Cache:一种用于补救冲突缺失的小Cache(通常4\sim16项),存放刚被从L1 Cache中驱逐的行。当L1缺失时,先检查Victim Cache,如果命中则将该行换回L1,避免了一次下级Cache或主存访问。

  • Store Buffer / Load Queue:乱序处理器中的存储缓冲区和加载队列,通常也采用CAM结构来进行地址匹配,以检测Store-to-Load转发和内存序冲突。这些结构的项数通常在32\sim128项之间。

Tag、Index和Offset的划分

对于一个给定配置的Cache,Tag、Index和Offset的位数可以通过简单的计算确定。下面通过一个具体的例子来演示。

假设一个Cache的参数为:

  • 地址位数:n=48n = 48位(现代64位处理器的典型虚拟地址宽度)

  • Cache总容量:C=32KB=32768C = 32\,\text{KB} = 32768字节

  • Cache行大小:B=64B=64B = 64\,\text{B} = 64字节

  • 相联度:W=8W = 8-way

计算过程:

  1. 总Cache行数 = C/B=32768/64=512C / B = 32768 / 64 = 512

  2. 组数 S=512/W=512/8=64S = 512 / W = 512 / 8 = 64

  3. Offset位数 b=log264=6b = \log_2 64 = 6

  4. Index位数 s=log264=6s = \log_2 64 = 6

  5. Tag位数 t=4866=36t = 48 - 6 - 6 = 36

因此,48位地址的划分为:

a47a12Tag: 36位a11a6Index: 6位a5a0Offset: 6位\underbrace{a_{47} \cdots a_{12}}_{\text{Tag: 36位}} \quad \underbrace{a_{11} \cdots a_{6}}_{\text{Index: 6位}} \quad \underbrace{a_{5} \cdots a_{0}}_{\text{Offset: 6位}}

考虑Intel Golden Cove的L1 D-Cache:48 KB/12-way,行大小64 B,48位虚拟地址。

  1. 总Cache行数 = 48×1024/64=76848 \times 1024 / 64 = 768

  2. 组数 S=768/12=64S = 768 / 12 = 64

  3. Offset = 6位,Index = 6位,Tag = 4866=3648 - 6 - 6 = 36

注意Index + Offset = 12位,恰好等于4 KB页面的页内偏移位数,这使得VIPT(虚拟索引、物理标记)访问可以正常工作。这并非巧合,48 KB/12-way的配置正是为满足此约束而精心选择的。

下面以一个64 KB、4-way、64 B行大小的Cache为例,逐步推导完整的地址位划分。假设使用48位物理地址。

第一步:计算Offset位数。Cache行大小B=64B = 64字节,行内任意字节的定位需要log264=6\log_2 64 = 6位,因此Offset = 6位a5a0a_5 \cdots a_0)。

第二步:计算组数和Index位数。

  • 总Cache行数 = C/B=65536/64=1024C / B = 65536 / 64 = 1024

  • 组数 = 1024/4=2561024 / 4 = 256

  • Index位数 = log2256=8\log_2 256 = 8位(a13a6a_{13} \cdots a_6

因此Index = 8位,共256个组。

第三步:计算Tag位数。t=4886=34t = 48 - 8 - 6 = 34位(a47a14a_{47} \cdots a_{14})。

48位地址的完整位字段划分如图图 5.10所示。

64 KB / 4-way / 64 B Cache 的 48 位地址划分
64 KB / 4-way / 64 B Cache 的 48 位地址划分

VIPT约束检查:Index + Offset = 8 + 6 = 14位,而4 KB页面的页内偏移只有12位。Index中的a13a_{13}a12a_{12}两位超出了页内偏移范围,这意味着在VIPT模式下会产生Alias问题。解决方案包括:使用16 KB页面(页内偏移为14位,恰好覆盖Index + Offset);或将相联度提高到16-way(每路64KB/16=4KB64\,\text{KB}/16 = 4\,\text{KB} \le页面大小);或使用硬件Anti-Aliasing。

各SRAM阵列的物理规格

  • Tag SRAM:256×(34+1+1+2)=256×38256 \times (34 + 1 + 1 + 2) = 256 \times 38位/路,4路共256×38×4=38912256 \times 38 \times 4 = 389124.75KB\approx 4.75\,\text{KB}

  • Data SRAM:256×512256 \times 512位/路,4路共256×512×4=524288256 \times 512 \times 4 = 524288=64KB= 64\,\text{KB}

  • Tag SRAM占Data SRAM的比例:4.75/647.4%4.75 / 64 \approx 7.4\%

表 5.6列出了几种常见Cache配置的地址划分。

Cache配置组数Tag位数Index位数Offset位数Tag SRAM总量
32 KB, 8-way643666512×36=2.25KB512 \times 36 = 2.25\,\text{KB}
48 KB, 12-way643666768×36=3.375KB768 \times 36 = 3.375\,\text{KB}
64 KB, 8-way12835761024×35=4.375KB1024 \times 35 = 4.375\,\text{KB}
64 KB, 16-way6436661024×36=4.5KB1024 \times 36 = 4.5\,\text{KB}
32 KB, 4-way1283576512×35=2.19KB512 \times 35 = 2.19\,\text{KB}
1 MB, 16-way10243210616384×32=64KB16384 \times 32 = 64\,\text{KB}
2 MB, 8-way40963012632768×30=120KB32768 \times 30 = 120\,\text{KB}

不同Cache配置的地址划分(48位虚拟地址,64B Cache行)

值得注意的是,在虚拟索引、物理标记(VIPT)的L1 Cache中,Index字段的位必须全部落在页内偏移(Page Offset)的范围之内,以避免同义问题(Aliasing)。对于4 KB页面大小,Page Offset为12位(a11a0a_{11} \cdots a_0),因此Index + Offset的总位数不能超过12位。这意味着在64 B行大小下,Index最多为6位,组数最多为64。如果Cache容量为CC、相联度为WW,则VIPT无别名约束要求:

CW页面大小 \frac{C}{W} \le \text{页面大小}

例如:

  • 32 KB的Cache要求W32768/4096=8W \ge 32768 / 4096 = 8-way。

  • 48 KB的Cache要求W48/4=12W \ge 48 / 4 = 12-way(Intel Golden Cove的L1D正是48 KB/12-way)。

  • 64 KB的Cache需要W16W \ge 16-way或使用更大的页面。Apple的64 KB(和128 KB)L1D使用16 KB页面来满足此约束。

VIPT的工作原理与优势

现代处理器使用虚拟地址(VA)进行存储器访问,但Cache中的Tag必须使用物理地址(PA)来保证正确性(因为多个VA可能映射到同一个PA)。这就引出了四种可能的索引/标记方式组合:PIPT(物理索引、物理标记)、VIVT(虚拟索引、虚拟标记)、VIPT(虚拟索引、物理标记)和PIVT(物理索引、虚拟标记,实际中不使用)。

VIPT之所以成为L1 Cache的主流选择,核心原因在于它允许TLB查找与Cache读取并行进行

  1. 处理器生成一个虚拟地址VA。

  2. VA的低位(Offset + Index)同时被送入Cache的Tag SRAM和Data SRAM进行索引——这些低位属于页内偏移,在VA和PA中完全相同(前提是Index位不超出页内偏移范围)。

  3. 与此同时,VA的高位被送入TLB进行虚拟地址到物理地址的转换,得到PA的高位(即PPN,Physical Page Number)。

  4. 当Cache的Tag SRAM读出所有路的Tag值时,TLB也恰好完成了地址转换。将转换得到的PA高位(即物理Tag)与读出的Tag值进行比较,确定是否命中。

这种并行性使得VIPT Cache的访问延迟与PIPT相比可以节省一个TLB查找延迟(通常为1\sim2个周期),这对L1 Cache的关键路径至关重要。

图 5.11展示了VIPT与PIPT两种方式的时序对比。

VIPT与PIPT的时序对比
VIPT与PIPT的时序对比

从图图 5.11中可以清楚地看到,VIPT方式将TLB查找的延迟完全隐藏在SRAM读取的背后,而PIPT方式必须先完成TLB查找才能开始SRAM读取(因为PIPT使用PA的Index位来索引SRAM,而PA需要TLB转换后才能得到)。在典型配置下,这个差异约为1\sim2个周期,对L1 Cache的有效延迟影响显著。

VIPT的Alias问题

当Cache每路的大小(C/WC/W)超过页面大小时,Index字段中有些位会落在页内偏移之外——这些位在VA和PA中可能不同。此时,同一个物理地址可能因为VA不同而映射到Cache中不同的组,形成多个副本,这就是Alias问题(也称Synonym问题)。

以一个64 KB、4-way、64 B行大小的Cache在4 KB页面下为例:

  • 每路大小 = 64KB/4=16KB64\,\text{KB} / 4 = 16\,\text{KB}

  • 组数 = 16KB/64B=25616\,\text{KB} / 64\,\text{B} = 256

  • Index位数 = log2256=8\log_2 256 = 8位(a13a6a_{13} \cdots a_6

  • Offset位数 = 6位(a5a0a_5 \cdots a_0

  • Index + Offset = 14位(a13a0a_{13} \cdots a_0

而4 KB页面的页内偏移只有12位(a11a0a_{11} \cdots a_0)。因此Index中的a13a_{13}a12a_{12}两位落在了页号范围内——这两位在VA和PA中可能不同。具体来说,两个不同的虚拟地址VA1\text{VA}_1VA2\text{VA}_2,如果映射到同一个物理页帧但a13a_{13}a12a_{12}不同,就会索引到Cache的不同组中,导致同一物理地址在Cache中出现两个副本。

这种Alias会导致数据不一致:如果一个副本被修改了,另一个副本仍然是旧值。在多进程共享内存或单进程中使用mmap映射同一文件到不同虚拟地址的场景下,Alias问题会导致严重的正确性错误。

Alias的解决方案

解决VIPT Alias问题有以下几种方案:

方案一:限制每路大小不超过页面大小。这是最简单也最常用的方案。通过选择合适的容量和相联度组合,确保C/WC/W \le页面大小。在4 KB页面下:

配置每路大小是否满足约束代表处理器
32 KB, 8-way4 KBAMD Zen 4
48 KB, 12-way4 KBIntel Golden Cove
64 KB, 16-way4 KB
64 KB, 4-way16 KB需要特殊处理

这种方案的代价是限制了Cache的设计空间:如果想要较大的Cache容量,就必须增加相联度(如48 KB必须至少12-way),而高相联度增加了MUX延迟和Tag比较的硬件复杂度。

方案二:使用大页面。如果操作系统使用16 KB的页面大小(如Apple的macOS/iOS),页内偏移扩大到14位(a13a0a_{13} \cdots a_0),则64 KB/4-way Cache的Index + Offset = 14位恰好不超出页内偏移,无Alias问题。Apple M系列处理器的L1D Cache高达128 KB/8-way,每路16 KB,恰好等于16 KB的页面大小,完全避免了Alias问题。

方案三:页着色(Page Coloring)。操作系统在分配物理页帧时,确保同一虚拟页面的"颜色位"(即落在VA的Index范围内但在页内偏移之外的地址位)与物理页帧对应的位相同。这样虽然VA和PA的高位不同,但用于Cache索引的位是相同的,消除了Alias。页着色的缺点是约束了操作系统的物理内存分配策略,降低了内存利用效率。早期的MIPS R10000和某些ARM处理器采用了这种方案。

方案四:硬件Anti-Aliasing。Cache硬件检测并处理Alias冲突。一种方法是在L1 Cache缺失时,额外检查其他可能存在Alias的组是否持有同一物理地址的副本。如果发现,将旧副本无效化(Invalidate)或合并。这需要额外的比较逻辑和可能的多组访问,增加了硬件复杂度和缺失处理延迟,但提供了最大的设计灵活性。部分ARM处理器(如Cortex-A76及后续架构)在L1 D-Cache中实现了这种硬件Anti-Aliasing机制。

这些VIPT相关约束将在第 6.0 章中进一步深入讨论。

路的选择电路

在并行访问的组相联Cache中,所有WW路的Data SRAM都会被读出,但最终只有命中路的数据被使用。路的选择(Way Selection)电路负责根据Tag比较结果从WW路数据中选出正确的数据。

Way MUX

最直接的方式是使用一个WW:1的多路选择器(MUX)。Tag比较产生一个WW位的独热码(one-hot)命中向量(例如在4-way Cache中为0100,表示Way 2命中),这个独热码作为MUX的选择信号,从WW路数据中选出命中路的数据。

Way MUX位于Cache访问的关键路径上(在Tag比较之后),其延迟直接影响Cache的总访问延迟。对于WW-way Cache,MUX的延迟约为O(log2W)O(\log_2 W)个逻辑门延迟。对于8-way Cache,MUX延迟约为3级逻辑门,在5 nm工艺下约为30 ps\sim50 ps。

为了降低MUX的数据宽度(进而降低面积和功耗),通常不会在MUX中传输整个Cache行(64 B = 512位),而是在Data SRAM内部使用Offset的高位进行列选择,只读出Cache行中需要的部分数据(如一个64位的双字),然后在MUX中只选择64位的数据。这将MUX的宽度从512位降低到64位,大幅减少了面积和功耗。

数据对齐与字节选择

Way MUX输出的数据通常是一个固定宽度的数据块(如64 bit的双字或128 bit的向量),但处理器的Load指令可能请求不同宽度的数据(1字节、2字节、4字节或8字节),且目标字节可能从Cache行中的任意对齐位置开始。数据对齐(Data Alignment)电路负责从Way MUX输出的数据块中提取出指令请求的字节,并进行适当的对齐和扩展。

数据对齐电路的主要功能包括:

  • 字节提取:根据Offset的低位(a2a1a0a_2 a_1 a_0,确定8字节块内的起始位置),从64位数据块中选出Load指令需要的1/2/4/8字节。对于64位数据块内的字节选择,需要一个桶形移位器(Barrel Shifter)或多级MUX。

  • 符号扩展/零扩展:对于有符号Load(如RISC-V的LBLHLW指令),需要对提取出的数据进行符号扩展到64位。对于无符号Load(如LBULHULWU),则进行零扩展。

  • 非对齐访问处理:当Load请求跨越64位双字边界时(例如从Offset=6开始读取4字节,跨越了字节7和字节8的边界),需要从两个相邻的双字中各取一部分并拼接。部分处理器通过硬件支持非对齐访问(需要额外的移位和拼接逻辑),部分处理器要求访问必须自然对齐(不对齐访问触发异常或拆分为两次访问)。

数据对齐电路位于Cache访问流水线的最后阶段,其延迟约为1\sim2级逻辑门(如果不考虑非对齐处理)。在大多数处理器中,对齐电路的延迟不是关键路径——关键路径通常在SRAM读取和Tag比较上。

Way Prediction

Way Prediction(路预测)是一种降低并行访问功耗的技术。其基本思想是:在访问Cache之前,先预测访问将命中哪一路,然后只读取预测路的Data SRAM。如果预测正确(称为Way Hit),则只消耗了一路的读取功耗,且延迟与并行访问相同;如果预测错误(称为Way Miss),则需要在下一周期重新读取正确路的Data SRAM,产生1个周期的额外延迟(这与Cache缺失不同,数据仍在Cache中,只是预测的路号错误)。

Way Prediction通常使用一个小型的Way Prediction表来实现,该表以Index为索引,存储每个组中最近一次命中的路号。表的大小等于Cache的组数SS,每项存储log2W\lceil \log_2 W \rceil位的路号。对于64组、8-way的Cache,表的大小为64×3=19264 \times 3 = 192位,硬件开销极小。

由于时间局部性,连续对同一组的访问很可能命中同一路,因此Way Prediction的准确率通常可以达到85%\sim95%。这意味着在85%\sim95%的访问中,Data SRAM的读取功耗降低到了1/W1/W;在剩下的5%\sim15%的访问中,需要一个额外周期的延迟惩罚。

Way Prediction的有效性可以用以下公式估算:

Tway-predict=Tparallel+(1pcorrect)×TextraT_{\text{way-predict}} = T_{\text{parallel}} + (1 - p_{\text{correct}}) \times T_{\text{extra}}

其中pcorrectp_{\text{correct}}为预测准确率,TextraT_{\text{extra}}为预测错误时的额外延迟(通常为1周期)。对于95%的准确率,额外延迟仅为0.05×1=0.050.05 \times 1 = 0.05周期,对性能的影响微乎其微,但功耗节省可达60%\sim80%。

Alpha 21264处理器(1998年)是Way Prediction的早期采用者,它在64 KB/2-way L1 D-Cache中使用了Way Prediction。在现代移动处理器(如ARM Cortex-A系列)中,Way Prediction更是一种标准做法。一些处理器甚至在L1 I-Cache中也使用Way Prediction,虽然I-Cache的访问模式通常更加规律(因为顺序取指),但Way Prediction仍然可以带来可观的功耗节省。

硬件描述 4 — Way Prediction硬件实现

Way Prediction表的实现非常紧凑。以一个64组、8-way的L1 D-Cache为例:

  • 表大小:64项,每项3位(log28=3\lceil\log_2 8\rceil = 3),总共64×3=19264 \times 3 = 192=24B= 24\,\text{B}

  • 读端口:1个——在Cache访问的第一个时钟周期的早期阶段读出预测路号。

  • 写端口:1个——在Cache命中确认后更新预测路号。

  • 延迟:由于表非常小,读取延迟远低于Tag SRAM的解码延迟,可以在Index生成后的极早期就给出预测结果。

在流水线实现中,Way Prediction的时序如下:

  1. 周期NN:地址计算完成,Index生成。

  2. 周期NN(后半周期):Way Prediction表给出预测路号wpredw_{\text{pred}}。同时用Index开始访问预测路的Data SRAM和所有路的Tag SRAM。

  3. 周期N+1N+1:Tag SRAM读出完成,进行比较,确定实际命中路whitw_{\text{hit}}

  4. 周期N+1N+1(后半周期):如果wpred=whitw_{\text{pred}} = w_{\text{hit}},Data已经可用,直接输出;否则需要在周期N+2N+2重新读取正确路的Data SRAM。

多端口Cache的实现

现代超标量处理器每周期需要从L1 D-Cache发起多次并行的存储器访问——例如,一个6-wide乱序超标量处理器可能配备2\sim3个Load单元和1\sim2个Store单元,每个单元每周期都可能发起一次Cache访问。这要求L1 D-Cache能够每周期同时服务多个读请求和写请求。然而,标准的SRAM阵列通常只有一个读写端口(即每周期只能进行一次读或写操作)。如何用合理的硬件开销实现多端口Cache,是Cache微架构设计中的一个核心挑战。

实现多端口Cache主要有三种方式:真正的多端口SRAM、Cache复制和Multi-banking。下面逐一分析。

真正的多端口SRAM

最直接的方法是将SRAM位单元本身设计为多端口——每增加一个读/写端口,每个位单元需要增加额外的存取晶体管和位线。对于一个支持PP个端口的SRAM位单元,需要2P2P个额外的NMOS存取管和PP对差分位线(BL/BL\overline{\text{BL}}),再加上PP条独立的字线WL。因此一个PP端口6T基础位单元的晶体管数为4+2P4 + 2P个(4个反相器管 + 2P2P个存取管)。

以双端口(2R/2W)为例,每个位单元需要4+2×2=84 + 2 \times 2 = 8个晶体管(与8T位单元的晶体管数相同,但布局不同)。如果是2读1写共3个端口,则需要4+2×3=104 + 2 \times 3 = 10个晶体管。图图 5.12对比了不同端口数下位单元的面积增长。

多端口SRAM位单元面积随端口数的增长趋势
多端口SRAM位单元面积随端口数的增长趋势

真正的多端口SRAM面积增长的主要原因不仅仅是晶体管数量的增加,更关键的是布线拥塞:每增加一个端口,需要增加一对差分位线和一条字线穿过阵列,这些金属布线占据了大量的面积。SRAM位单元通常在最低几层金属层(M1\simM3)中实现,每增加一个端口所需的额外位线和字线可能将位单元面积增大50%\sim100%。考虑到布线的二维影响,真正的多端口SRAM面积近似以端口数的平方O(P2)O(P^2)增长。

此外,多端口SRAM还有以下问题:

  • 位线负载增加:更多的位线意味着更大的寄生电容和更慢的位线放电速度,增加了SRAM的访问延迟。

  • 同时写入冲突:如果两个写端口同时写入同一地址但数据不同,需要额外的仲裁逻辑来决定哪个写入优先。

  • 功耗增加:更多的位线预充电和放电消耗更多的动态功耗。

由于以上原因,真正的多端口SRAM通常只用于小容量、高端口需求的结构,如寄存器文件(Register File,通常有6\sim12个读端口和3\sim6个写端口,但容量仅为几百字节)和CAM(Content-Addressable Memory)。对于L1 D-Cache这种容量为32\sim128 KB的结构,真正的多端口SRAM的面积和延迟开销都不可接受。

Cache复制(Cache Copies/Replication)

Cache复制是一种以面积换端口数的方法:将整个Data SRAM复制多份,每份提供一个独立的读端口。所有副本的内容始终保持一致——当写操作发生时,同时写入所有副本。

以一个需要2个读端口 + 1个写端口的32 KB L1 D-Cache为例:

  • 复制Data SRAM两份:Copy A和Copy B。

  • Load端口0从Copy A读取,Load端口1从Copy B读取。

  • 每次Store操作同时写入Copy A和Copy B,保持两份副本一致。

  • 每份SRAM只需要1个读端口和1个写端口(标准的单端口SRAM加一个写端口即可实现),避免了真正多端口SRAM的复杂性。

Cache复制的优点是:

  • 每份SRAM仍然使用标准的6T位单元,不需要特殊的多端口位单元设计。

  • 每份SRAM的访问延迟与单端口SRAM相同,不会因为多端口而增加关键路径延迟。

  • 设计简单——只需要将写数据同时路由到所有副本即可。

Cache复制的主要缺点是面积翻倍(或更多):NN个读端口需要NN份Data SRAM副本,面积增长为NN倍。对于32 KB的Data SRAM,两份副本意味着64 KB的SRAM面积用于提供2个读端口——面积增长100%。相比之下,真正的2端口SRAM面积增长约120%\sim150%,因此Cache复制在面积上并不一定比真正的多端口差,但在延迟和设计简单性上有明显优势。

同步写入的一致性问题:当Load和Store同时访问同一地址时,Load应该读到Store的旧值还是新值?这取决于处理器的内存模型和流水线语义。通常,同周期的Store数据不能通过Cache Data SRAM读到(因为写入需要在周期末完成),而是通过Store-to-Load转发(Store Forwarding)旁路网络在Load/Store单元内部完成数据传递。因此,Cache复制中的写一致性延迟(写入传播到所有副本的延迟)通常不是关键问题——只要在下一个周期之前所有副本都更新完毕即可。

Alpha 21264处理器(1998年)是采用Cache复制方案的经典案例:它将64 KB的L1 D-Cache复制为两份(总共128 KB的SRAM面积),提供两个独立的Load读端口。这种方案在面积允许的情况下提供了最简单和最快速的多端口实现。

Multi-banking

Multi-banking(多体交叉)是现代高性能处理器L1 D-Cache中最广泛采用的多端口实现方案。其核心思想是:将Data SRAM分成多个独立的Bank,每个Bank只有一个读/写端口(即标准的单端口SRAM),但不同的端口可以在同一周期内访问不同的Bank,从而实现多端口的效果。

图 5.13展示了一个将Data SRAM分为B=8B = 8个Bank的Cache结构。

Multi-banking结构:Data SRAM分为8个Bank
Multi-banking结构:Data SRAM分为8个Bank
Bank的划分方式。

Bank的选择通常使用地址中Offset字段的高位或Index字段的低位。具体使用哪些位取决于设计目标:

  • 用Offset高位选Bank:例如对于64 B行大小、8个Bank的配置,每个Bank存储Cache行中的64/8=864/8 = 8字节。用Offset的高3位(a5a4a3a_5 a_4 a_3)选Bank。这种方式的好处是同一Cache行的不同部分分布在不同的Bank中,连续地址的访问不会冲突。但如果两个Load请求恰好需要同一Cache行的相同8字节区域,仍会发生Bank冲突。

  • 用Index低位选Bank:例如用Index的低3位(a8a7a6a_8 a_7 a_6)选Bank。这种方式下,不同的Cache组映射到不同的Bank。如果两个访问来自不同的Cache组,则不会发生Bank冲突。但如果两个访问恰好映射到同一组的同一Bank,就会冲突。

在实际设计中,许多处理器使用Offset的高位来选择Bank(即字交叉,Word Interleaving),因为Load指令通常访问同一Cache行内的不同字(如结构体的不同字段),这种划分方式可以最大化并行度。

Bank冲突的概率分析。

当两个端口在同一周期试图访问同一个Bank时,就会发生Bank冲突(Bank Conflict)。由于每个Bank只有一个端口,两个请求无法同时被服务,必须将其中一个推迟到下一周期。

假设BB个Bank中每个Bank被访问的概率相等(即地址均匀分布),则两个端口访问同一Bank的概率为:

Pconflict=1B P_{\text{conflict}} = \frac{1}{B}

对于B=8B = 8个Bank,Pconflict=1/8=12.5%P_{\text{conflict}} = 1/8 = 12.5\%。对于B=16B = 16个Bank,Pconflict=1/16=6.25%P_{\text{conflict}} = 1/16 = 6.25\%

如果有KK个端口同时访问BB个Bank,至少一次Bank冲突发生的概率可以近似为:

Pany conflict1B!(BK)!BK=1i=0K1BiBP_{\text{any conflict}} \approx 1 - \frac{B!}{(B-K)! \cdot B^K} = 1 - \prod_{i=0}^{K-1}\frac{B-i}{B}

对于K=2K = 2Pany conflict=1(B1)/B=1/BP_{\text{any conflict}} = 1 - (B-1)/B = 1/B,与上式一致。对于K=3K = 3B=8B = 8Pany conflict=1(8×7×6)/(83)=1336/51234.4%P_{\text{any conflict}} = 1 - (8 \times 7 \times 6)/(8^3) = 1 - 336/512 \approx 34.4\%

性能分析 2 — Bank冲突对性能的影响

考虑一个2-Load端口的L1 D-Cache,Data SRAM分为8个Bank。假设平均每周期有1.5个Load请求(即50%的周期有2个Load)。当两个Load同时发出时,Bank冲突的概率为1/8=12.5%1/8 = 12.5\%。因此,由于Bank冲突导致的有效Load吞吐量损失约为:

损失=0.5×0.125×1=0.0625 Load/周期\text{损失} = 0.5 \times 0.125 \times 1 = 0.0625 \text{ Load/周期}

即平均每16个周期损失1个Load的吞吐量。在实际工作负载中,由于地址分布并非完全均匀(连续数组访问往往分布在不同的Bank中),实际的Bank冲突率通常低于理论值。

Bank冲突的解决方式。

当Bank冲突发生时,通常采用以下策略之一:

  • 暂停一个端口:将冲突的请求之一推迟到下一周期重新发射。优先级通常给予较老的指令(即先到的请求优先)。被暂停的Load指令会在下一周期重新尝试访问Cache。

  • 使用仲裁器(Arbiter):一个集中式的仲裁逻辑在每周期开始时检查所有端口的Bank访问请求,检测冲突并决定优先级。仲裁器的延迟通常很短(1\sim2级逻辑门),可以在地址计算完成后的极早期给出仲裁结果。

  • Bank冲突预测:在调度阶段提前预测Bank冲突,避免同时调度可能冲突的两条Load指令。这需要在调度器中维护Bank地址信息,增加了调度器的复杂度。

案例研究 1 — AMD Opteron的L1 D-Cache Multi-banking结构

AMD Opteron(K8微架构,2003年)的L1 D-Cache是Multi-banking方案的经典实现案例。其配置如下:

  • 总容量:64 KB,2-way组相联,64 B行大小

  • 组数:64KB/(64B×2)=51264\,\text{KB} / (64\,\text{B} \times 2) = 512

  • 端口需求:2个Load + 1个Store(每周期最多3次Data SRAM访问)

  • Data SRAM分为8个Bank,每个Bank容量为64KB/8=8KB64\,\text{KB} / 8 = 8\,\text{KB}

  • Bank选择位:使用Offset的高3位(a5a4a3a_5 a_4 a_3),每个Bank存储Cache行中的8字节

图 5.14展示了AMD Opteron L1 D-Cache的完整结构框图。

在Opteron的设计中,两个Load端口和一个Store端口共享8个Data Bank。在理想情况下,三个端口访问不同的Bank,可以无冲突地并行服务。当两个端口试图访问同一Bank时,仲裁器将一个请求推迟一个周期。由于Bank数(8)远大于端口数(3),Bank冲突的概率较低(理论上约为1(8×7×6)/(83)34%1 - (8 \times 7 \times 6)/(8^3) \approx 34\%,但实际工作负载中由于空间局部性的存在,连续的Load往往访问不同的Bank,实际冲突率通常低于20%)。

这种Multi-banking方案的面积开销远小于Cache复制(无需复制Data SRAM)和真正的多端口SRAM(使用标准6T位单元),同时提供了足够好的多端口性能。因此,Multi-banking成为了现代L1 D-Cache多端口实现的主流方案。Intel、AMD和ARM的大多数高性能处理器核心都采用了类似的Multi-banking结构,Bank数量通常为8\sim16个。

Multi-banking的地址位映射详解

为了更清楚地理解Multi-banking的地址映射,下面以一个32 KB/8-way/64 B行大小/8-Bank的Cache为例,展示完整的地址位分配。

Cache参数:组数 = 32768/(64×8)=6432768 / (64 \times 8) = 64组,Offset = 6位,Index = 6位,Tag = 36位(48位地址)。Data SRAM分为8个Bank,使用Offset的高3位选Bank。

完整的48位地址被划分为如下字段:

a47a12Tag: 36位a11a6Index: 6位a5a4a3Bank: 3位a2a1a0字节偏移: 3位\underbrace{a_{47} \cdots a_{12}}_{\text{Tag: 36位}} \quad \underbrace{a_{11} \cdots a_{6}}_{\text{Index: 6位}} \quad \underbrace{a_{5} a_{4} a_{3}}_{\text{Bank: 3位}} \quad \underbrace{a_{2} a_{1} a_{0}}_{\text{字节偏移: 3位}}

其中Offset字段的6位被进一步拆分为两部分:

  • Bank选择位a5a4a3a_5 a_4 a_3):选择8个Bank之一。每个Bank存储Cache行中的64/8=864/8 = 8字节。

  • 字节偏移a2a1a0a_2 a_1 a_0):选择Bank内8字节中的具体字节位置。

图 5.15展示了一个Cache行内数据到8个Bank的映射关系。

Cache行中数据在8个Bank中的交叉分布
Cache行中数据在8个Bank中的交叉分布

这种字交叉(Word Interleaving)方式的好处在于:如果程序顺序访问一个数组的连续元素(如a[0], a[1], a[2], ...),相邻元素通常落在不同的Bank中(除非元素大小恰好等于Bank宽度的整数倍)。例如,对于4字节的int数组,a[0]在Bank 0的字节0–3,a[1]在Bank 0的字节4–7,a[2]在Bank 1的字节0–3,以此类推。两个Load如果分别访问a[0]a[2],它们会访问不同的Bank(Bank 0和Bank 1),可以并行完成。

然而,如果两个Load都访问同一个8字节块(如同时访问一个struct的两个相邻字段,且这两个字段恰好在同一个Bank内),就会发生Bank冲突。在实际程序中,这种情况的概率取决于数据结构的布局和访问模式,通常在5%\sim15%之间。

Bank数量的选择

Bank数量BB的选择需要在以下因素之间权衡:

  • 冲突概率:Bank数量越多,冲突概率越低。对于2个端口,Pconflict=1/BP_{\text{conflict}} = 1/B。从B=4B=4(25%冲突率)到B=8B=8(12.5%)到B=16B=16(6.25%)。

  • Bank粒度:每个Bank存储的字节数 = Bline/BB_{\text{line}} / BBB越大,每个Bank存储的数据越少(粒度越细)。如果Bank粒度小于处理器的最大Load宽度(如AVX-512需要64 B的Load,涉及所有8个Bank),则一次宽Load需要同时访问多个Bank,增加了控制复杂度。

  • 路由网络面积KK个端口和BB个Bank之间需要一个K×BK \times B的Crossbar路由网络,其面积和延迟随K×BK \times B增长。BB过大会使Crossbar成为面积和延迟的瓶颈。

  • 每个Bank的SRAM面积:Bank数量越多,每个Bank的SRAM阵列越小,读取延迟越低(因为位线和字线更短)。但Bank太小(如只有几百位)时,外围电路的面积开销占比增大,面积效率下降。

在实际处理器中,B=8B = 8是最常见的选择:它在冲突概率(12.5%)、Bank粒度(8字节,恰好等于一个64位双字)和Crossbar复杂度之间提供了良好的平衡。部分高性能处理器(如Intel的某些核心)使用B=16B = 16个Bank以进一步降低冲突概率,但需要更复杂的路由网络。

值得注意的是,当处理器支持向量/SIMD访问(如x86的AVX-256或AVX-512,ARM的SVE)时,单次向量Load可能需要访问32 B甚至64 B的数据,这涉及到4个甚至8个Bank的同时读取。此时,向量Load不能与标量Load共享Bank——如果向量Load占用了4个Bank,同周期的标量Load只能访问剩余的4个Bank,否则会产生冲突。一些处理器通过为向量访问提供单独的数据通路(绕过Bank选择逻辑直接读取整个Cache行)来解决这个问题,但这增加了数据通路的宽度和面积。

设计权衡 2 — 三种多端口Cache方案的比较

表 5.7总结了三种多端口Cache实现方案的对比。在实际设计中,大多数高性能处理器的L1 D-Cache采用Multi-banking方案,寄存器文件采用真正的多端口SRAM,而Cache复制偶尔用于特殊场合。

特性真正多端口SRAMCache复制Multi-banking
面积增长O(P2)O(P^2),最大O(P)O(P),较大O(1)O(1),最小
延迟影响增加(位线负载)无影响无影响
设计复杂度高(特殊位单元)低(标准SRAM)中(仲裁逻辑)
Bank冲突有(概率1/B1/B
写一致性内在一致需多写内在一致
适用容量<1KB< 1\,\text{KB}32KB32\,\text{KB}128KB128\,\text{KB}32KB32\,\text{KB}128KB128\,\text{KB}
典型应用寄存器文件Alpha 21264 L1D现代L1 D-Cache

在结束Cache组成方式的讨论之前,需要提及多级Cache之间的包含策略(Inclusion Policy),即上级Cache中的数据是否一定存在于下级Cache中。常见的包含策略有三种:

  • 包含式(Inclusive):上级Cache的所有内容都包含在下级Cache中。即L1中的每一行在L2中都有一份副本。优点是一致性查询(Snoop)只需要检查LLC即可——如果LLC中没有某行,则L1/L2中一定也没有。缺点是有效容量浪费——L2中有一部分空间被用于保存L1中已有的数据副本,实际可用容量为CL2CL1C_{\text{L2}} - C_{\text{L1}}。当L2行被驱逐时,必须同时无效化L1中的对应行(称为Back-Invalidation),这可能导致L1中有用数据的意外丢失。Intel从Nehalem(2008年)到Ice Lake(2019年)的LLC都采用包含式设计。

  • 排他式(Exclusive):一个Cache行只存在于某一级Cache中,不会同时存在于多级中。当L1缺失且L2命中时,该行从L2移动到L1(L2中不再保留副本);当L1行被驱逐时,该行被移入L2(而非丢弃)。排他式Cache最大化了总有效容量(CL1+CL2C_{\text{L1}} + C_{\text{L2}}),但一致性查询需要检查所有级别的Cache,增加了Snoop的复杂度。AMD的Zen系列处理器在L2和L3之间采用排他式设计。

  • 非包含非排他式(NINE,Non-Inclusive Non-Exclusive):不强制包含或排他的约束。L1中的行可能在L2中有副本也可能没有。这提供了最大的灵活性但使一致性管理更复杂,通常需要额外的Snoop Filter(一种紧凑的Tag目录)来追踪哪些行存在于哪一级Cache中。Intel从Skylake-SP开始在LLC中采用NINE设计。

表 5.8总结了三种包含策略的关键特性对比。

特性包含式排他式NINE
有效容量CL2C_{\text{L2}}CL1+CL2C_{\text{L1}} + C_{\text{L2}}介于两者之间
Snoop复杂度低(只查LLC)高(查所有级)中(需Snoop Filter)
Back-Invalidation
数据一致性维护简单较复杂复杂
代表处理器Intel Nehalem–Ice LakeAMD Zen系列Intel Skylake-SP+

多级Cache包含策略的比较

以包含式Cache为例说明Back-Invalidation问题。假设L1为32 KB/8-way,L2为256 KB/8-way,采用包含策略。

当L2中一行被驱逐时(例如由于L2缺失需要腾出空间),如果该行在L1中也有副本,则必须同时无效化L1中的对应行。这就是Back-Invalidation。

问题在于:被Back-Invalidation删除的L1行可能正是一个频繁使用的热点数据。如果L2的相联度不够高,或者工作集恰好导致特定的冲突模式,Back-Invalidation可能频繁发生,造成L1的有效命中率下降。

为了缓解这个问题,包含式Cache通常要求L2的路数\ge L1的路数。例如,如果L1有8路,L2至少也需要8路,以确保L2中为L1的每一路都保留了空间。如果L2的路数少于L1,可能出现一种极端情况:L1的某个组中8个有效行映射到L2的同一组,但L2的组只有4路,容不下所有8行,导致不断的Back-Invalidation和L1行的意外丢失。

包含策略的选择直接影响到Cache层次的有效容量、一致性协议的复杂度和性能。这些内容将在第 11.0 章中深入讨论。

在讨论写入策略之前,简要说明Cache缺失(Miss)发生时的基本处理流程。当处理器发起一次Load操作且L1 D-Cache缺失时,处理步骤如下:

  1. 根据替换策略选择一个牺牲行(Victim Line)。如果牺牲行是Dirty的,将其数据暂存到Write-Back Buffer中等待写回下级存储。

  2. 向L2 Cache(或下一级存储)发起缺失请求(Miss Request),请求目标Cache行的数据。

  3. 当下级存储返回数据后,将数据写入L1 Cache中牺牲行的位置,更新Tag和Valid位。

  4. 将处理器请求的字(通常为64位双字)从新填入的Cache行中取出,送回处理器的加载单元。

一个重要的优化是关键字优先(Critical Word First)和提前重启(Early Restart)。在关键字优先策略下,下级存储首先返回处理器实际请求的那个字(而非从Cache行的起始地址开始传输),处理器收到关键字后立即恢复执行(不需要等待整个Cache行的传输完成)。随后,Cache行的剩余部分继续从下级存储传入。这可以将缺失延迟减少数个周期——对于64 B的Cache行和16 B/周期的传输带宽,完整传输需要4个周期,但关键字优先可以在第1个周期就将需要的数据送给处理器。现代所有高性能处理器都实现了这种优化。

缺失处理的另一个重要方面是非阻塞(Non-Blocking)操作:现代处理器的L1 D-Cache通常是非阻塞的——在一个缺失正在处理的过程中,Cache仍然可以服务后续的命中请求,甚至可以接受新的缺失请求(如果硬件资源允许)。这种能力依赖于MSHR(Miss Status Holding Register)硬件来管理多个并行的未完成缺失。

非阻塞Cache的基本思想是将缺失处理与Cache的正常访问解耦。当一次Cache缺失发生时,不是阻塞整个Cache直到缺失处理完成,而是在MSHR中记录该缺失的信息(包括缺失地址、请求来源、目标Cache行位置等),然后允许Cache继续处理后续的访问请求。MSHR的典型配置如下:

  • MSHR项数:8\sim16项。每项对应一个正在处理的缺失。Intel Golden Cove的L1D有12个MSHR项。

  • 每项内容:缺失地址的Tag和Index、目标Way号、请求者信息(如Load队列项号)、状态(已发送请求/数据返回中/填充完成等)。

  • 合并功能:如果多个Load指令对同一Cache行发生缺失(称为Secondary Miss),不需要分配新的MSHR项——而是将后续缺失合并到已有的MSHR项中(记录额外的请求者信息)。当数据返回时,MSHR同时唤醒所有等待该行的请求者。这种合并避免了对下级存储的重复请求,大幅减少了缺失处理的带宽需求。

当所有MSHR项都被占用(即同时有最大数量的缺失正在处理)时,新的缺失请求将被阻塞,直到有MSHR项被释放。MSHR项数的选择需要在硬件开销和并行缺失处理能力之间权衡——更多的MSHR项允许更多的并行缺失(通过乱序执行和预取产生),但增加了硬件复杂度和面积。

非阻塞Cache对于乱序处理器的性能至关重要。一个阻塞Cache在缺失发生时会完全暂停数据供给,而L1缺失到L2的延迟约为10\sim14个周期——在这段时间内,一个6-wide处理器可能浪费60\sim84条指令的执行机会。非阻塞Cache允许处理器在等待缺失处理的同时继续执行不依赖于缺失数据的指令,通过Memory-Level Parallelism(存储级并行,MLP)来隐藏缺失延迟。非阻塞Cache的详细设计将在第 7.0 章中深入讨论。

Cache的写入策略

当处理器执行一条存储(Store)指令时,需要将数据写入Cache。Cache的写入策略(Write Policy)决定了数据何时被传播到下一级存储,以及在写缺失时如何处理。写入策略由两个正交的维度组成:写更新策略(Write Hit Policy,即写命中时的行为)和写分配策略(Write Miss Policy,即写缺失时的行为)。

写回(Write Back)

写回(Write Back)策略下,Store指令的数据只写入当前级的Cache,而不立即传播到下一级存储。被修改过的Cache行通过一个Dirty位(脏位)来标记。每个Cache行增加1位的Dirty标志,初始状态为0(Clean),当该行被Store操作修改后设置为1(Dirty)。当一个Dirty的Cache行被替换(驱逐)时,其数据才会被写回到下一级存储;如果该行不是Dirty的(即从未被修改过或已被写回),则可以直接丢弃,无需写回。

写回策略通常与写分配(Write Allocate)策略配合使用:当发生写缺失时,先从下一级存储中将目标行取入Cache,然后在Cache中执行写操作。这一组合是现代高性能处理器L1/L2 Cache的主流选择。图图 5.16展示了其工作流程。

Write Back + Write Allocate策略的工作流程
Write Back + Write Allocate策略的工作流程

写回策略的优点包括:

  • 减少写流量:如果一个Cache行在被驱逐之前被多次修改,这些修改只产生一次写回操作。对于频繁写入的数据(如循环计数器、缓冲区指针等),写回策略可以大幅减少对下级存储的写带宽需求。

  • 减少总线占用:由于不需要每次Store都访问下级存储,写回策略可以将宝贵的Cache间总线带宽留给缺失处理(取回新行)和一致性流量。

写回策略的缺点包括:

  • 硬件复杂度增加:需要维护每行的Dirty位。在驱逐Dirty行时,需要将其数据写回下级存储,这需要一个Write-Back Buffer(写回缓冲)来暂存待写回的数据,避免阻塞后续的缺失处理。

  • 数据一致性维护复杂:在多核系统中,当一个核修改了某个Cache行(该行变为Dirty),其他核的Cache中可能持有该行的旧副本。需要通过缓存一致性协议(如MESI/MOESI)来维护各核之间数据的一致性。Dirty行对应MESI协议中的Modified状态。

  • 缺失处理可能更慢:如果需要驱逐的行是Dirty的,缺失处理需要先将该行写回(或同时写回和取回新行,称为Write-Back + Fetch的流水化),总延迟可能更长。

Write-Back Buffer的设计

Write-Back Buffer是写回策略中不可或缺的硬件组件。当一个Dirty的Cache行被驱逐时,它的数据不能直接丢弃——必须写回到下一级存储(L2 Cache或主存)。如果驱逐操作必须等待写回完成才能进行(即先写回旧行,再取回新行),缺失处理的延迟将增加一个完整的写回延迟(通常为8\sim14个周期到L2)。

Write-Back Buffer的作用就是将写回操作解耦(Decouple):被驱逐的Dirty行的地址和数据被暂存到Write-Back Buffer中,L1 Cache立即释放该行的位置用于新行的填入,而Write-Back Buffer中的数据会在后台异步地写回下一级存储。

Write-Back Buffer的典型配置如下:

  • 项数:4\sim8项。每项存储一个完整的Cache行地址和数据(如64 B数据 + 34位Tag + 8位Index = 每项约530位)。

  • 接口:一端连接L1 Cache的驱逐逻辑(接收被驱逐的Dirty行),另一端连接L2 Cache的写入端口(发出写回请求)。

  • 查询功能:当L1 Cache发生缺失时,除了查询L2 Cache外,还需要查询Write-Back Buffer——因为目标数据可能恰好在Write-Back Buffer中(刚被驱逐还未写回L2)。如果在Write-Back Buffer中命中,可以直接将数据返回给L1,节省了一次L2访问的延迟。这种情况虽然不常见(概率通常低于1%),但必须在硬件中正确处理以保证数据一致性。

当Write-Back Buffer满时(所有项都在等待写回L2),如果此时又需要驱逐一个Dirty行,L1 Cache必须暂停(Stall),直到Write-Back Buffer释放出空闲项。为了避免这种暂停,Write-Back Buffer的项数必须足够多,以覆盖写回延迟期间可能发生的驱逐数量。在实际设计中,4\sim8项的Write-Back Buffer已经足以满足大多数工作负载的需求,因为连续多个缺失都恰好驱逐Dirty行的概率较低。

写通(Write Through)

写通(Write Through)策略下,每次Store操作都同时写入当前级Cache和下一级存储。这样,Cache中的数据始终与下一级存储保持一致(至少对于写操作而言),不需要Dirty位。

写通策略通常与非写分配(No-Write Allocate,也称Write-Around或Write No-Allocate)策略配合使用:当发生写缺失时,不将目标行取入Cache,而是直接将数据写入下一级存储。图图 5.17展示了这一组合的工作流程。

Write Through + No-Write Allocate策略的工作流程
Write Through + No-Write Allocate策略的工作流程

写通策略的优点是设计简单、一致性维护简单(因为下一级存储始终持有最新数据,在多核系统中不需要处理Dirty行的写回问题)。其主要缺点是写带宽需求高:每次Store操作都要写入下一级存储,这会产生大量的写流量。

为了缓解这一问题,写通Cache通常配合一个Write Buffer(写缓冲)使用——Store操作先写入Write Buffer中的一个空闲项,处理器随即继续执行后续指令(不需要等待下级存储的写完成信号),由Write Buffer中的硬件逻辑异步地将数据传播到下一级存储。Write Buffer通常有4\sim16项,采用FIFO结构。如果Write Buffer满了,处理器必须暂停(Stall)直到有空闲项。

此外,Write Buffer还可以进行写合并(Write Coalescing/Write Combining):如果Write Buffer中已有一项的地址与新Store操作的地址位于同一Cache行,则将新数据合并到已有项中,而不是分配一个新项。这可以将多个对同一Cache行的小写操作合并为一个大的写操作,减少对下级存储的访问次数。

Write Buffer的硬件结构

图 5.18展示了一个典型的Write Buffer硬件结构。

Write Buffer的硬件结构(4项为例)
Write Buffer的硬件结构(4项为例)

Write Buffer的每一项包含以下字段:

  • Valid位:标记该项是否有效。

  • 地址:Store目标地址的Tag和Index部分(标识Cache行的地址)。

  • 数据:最多一个Cache行大小(64 B)的数据。

  • 字节有效位(Byte Valid Mask):64位的掩码,标记数据中哪些字节包含有效的待写出数据。一个32位Store指令只会设置4个字节的有效位,而经过多次写合并后,可能有更多的字节被设置为有效。

  • 状态:该项的处理状态(等待发送、正在发送、发送完成等)。

写合并过程:当一个新的Store请求到达Write Buffer时,首先将其地址与所有有效项的地址进行CAM匹配。如果找到一个匹配项(即已有一项属于同一Cache行),则将新Store的数据合并到该项的数据字段中(只更新对应字节),并将对应的字节有效位置位。如果没有匹配项,则分配一个新的Write Buffer项。

写合并在以下场景中特别有效:

  • memsetmemcpy等批量内存操作,连续的Store地址通常落在同一Cache行中。

  • 结构体字段的逐一写入,如p->x = 1; p->y = 2; p->z = 3;,三次Store可能合并为一次Cache行写入。

  • 循环中对同一变量的反复更新(虽然这种情况编译器通常会将变量保留在寄存器中)。

Store Buffer与写策略的关系

在乱序处理器中,Store Buffer(存储缓冲区,也称Store Queue)是一个比Write Buffer更复杂的结构。Store Buffer的主要功能是在Store指令提交(Commit/Retire)之前,暂存其地址和数据,确保在异常或分支预测错误导致的流水线冲刷时,这些未提交的Store不会污染Cache中的数据。

Store Buffer与Write Buffer的关系如下:

  • Store Buffer位于处理器核心的执行阶段和L1 D-Cache之间,保存尚未提交的Store数据。Store Buffer的项数通常为48\sim128项(如Intel Golden Cove为72项,AMD Zen 4为64项),支持CAM匹配以实现Store-to-Load转发。

  • Write Buffer位于L1 D-Cache和L2 Cache之间,保存已提交但尚未写入下级存储的写数据。Write Buffer的项数通常为4\sim16项。

  • 当一条Store指令提交时,其数据从Store Buffer中写入L1 D-Cache(如果命中)或Write Buffer(如果L1缺失或采用写通策略)。

在Write Back策略下,Store Buffer中提交的Store数据直接写入L1 D-Cache的Data SRAM(更新对应的字节并设置Dirty位),只有在L1 Cache行被驱逐时,脏数据才通过Write-Back Buffer写回L2。因此,Write Back策略下L1 D-Cache可以吸收绝大部分的写流量,Write Buffer的压力很小。

在Write Through策略下,Store Buffer中提交的Store数据既要写入L1 D-Cache,又要通过Write Buffer传播到L2。Write Buffer在此情况下承受了全部的写流量,写合并成为缓解Write Buffer压力的关键技术。

在现代高性能处理器中,L1 Cache几乎总是采用写回策略。这是因为在5 GHz的处理器上,每周期可能有1\sim2次Store操作,如果每次都要写入L2,L1到L2之间的总线需要承受极高的写带宽(例如每周期2次×\times8字节 = 16 B的纯写带宽,加上读缺失的带宽,总线将严重拥堵)。写回策略可以将绝大多数的写操作吸收在L1中,只在L1行被驱逐时才产生写流量。

写通策略偶尔用于GPU的L1 Cache(因为GPU的Store模式通常是流式写入,时间局部性差,不值得为Dirty行管理付出额外的硬件复杂度)或某些嵌入式处理器的L1 Cache(设计简单性优先)。

写分配与非写分配

写缺失时是否将目标行取入Cache,这就是写分配(Write Allocate)与非写分配(No-Write Allocate)的选择。

  • Write Allocate:写缺失时,先从下一级存储取回整个Cache行到Cache中,然后在Cache中修改对应的字节。这种方式利用了空间局部性——后续对同一Cache行中其他字节的访问可以在Cache中命中。Write Allocate通常与Write Back策略配合使用,因为取回的行可能会被后续的Store再次修改,使用Write Back可以避免每次修改都写回下级存储。

  • No-Write Allocate(Write Around):写缺失时,不取回Cache行,直接将数据写入下一级存储。这种方式避免了取回一个可能不会再被访问的Cache行(如某些只写一次的数据、DMA目标缓冲区等),但如果后续有读操作访问同一Cache行,会产生读缺失。No-Write Allocate通常与Write Through策略配合使用。

在Write Allocate策略中,有一种特殊情况值得注意:当Store操作覆盖了整个Cache行时(例如memset或DMA传输),从下级存储取回旧数据是不必要的,因为旧数据会被完全覆盖。部分处理器通过Write Allocate with No-Fetch优化来避免这次不必要的取回——如果处理器能检测到Store操作将覆盖整个Cache行,则直接分配一个新的Cache行并标记为Dirty,而不从下级存储取回旧数据。ARM架构提供了专门的DC ZVA指令(Data Cache Zero by Virtual Address)来将一个Cache行清零并分配,就利用了这种优化。

Write Allocate的延迟分析

Write Allocate策略下的写缺失处理比读缺失更复杂,因为它涉及到两个潜在的内存操作——写回旧行和取回新行。图图 5.19展示了写缺失处理的时序。

Write Allocate写缺失处理的时序分析
Write Allocate写缺失处理的时序分析

当被驱逐的行是Clean的(未被修改过),处理只需要从L2取回新行,延迟约为L2的访问延迟(10\sim14个周期)。当被驱逐的行是Dirty的,需要同时进行写回和取回两个操作。在大多数处理器中,这两个操作可以并行进行——Write-Back Buffer接收旧行数据的同时,L2 Cache开始处理取回请求。只要L2有足够的带宽同时处理一个读请求和一个写请求(大多数L2 Cache都有独立的读写端口或支持分时复用),Dirty驱逐的延迟就不会比Clean驱逐长。

如果L2的读写端口存在冲突(如只有单端口),则写回和取回必须串行进行,总延迟约为2×TL22 \times T_{L2}。为了避免这种情况,现代处理器的L2 Cache通常至少提供2个端口(1读1写),或使用Multi-banking来支持并行的读写操作。

表 5.9总结了常见的写入策略组合。

策略组合写命中写缺失典型应用
WB + WA只写Cache取回行后写CacheL1/L2 Cache(主流)
WT + NWA写Cache + 写下级直接写下级嵌入式L1、GPU L1
WB + NWA只写Cache直接写下级少见
WT + WA写Cache + 写下级取回行后写两处少见

Cache写入策略组合总结

案例研究 2 — RISC-V Cache管理指令

RISC-V的Zicbom扩展定义了三条Cache Block Management指令:

  • CBO.CLEAN:将指定Cache行的数据写回下级存储(如果是Dirty的),但保留在Cache中。这对应于将Cache行从Modified状态转变为Exclusive状态。

  • CBO.FLUSH:将指定Cache行的数据写回下级存储(如果是Dirty的),并从Cache中无效化(Invalidate)。相当于Clean + Invalidate。

  • CBO.INVAL:将指定Cache行从Cache中无效化,不写回。如果该行是Dirty的且未被写回,数据将丢失。这通常用于DMA操作前使Cache中的旧数据无效。

此外,Zicboz扩展定义了CBO.ZERO指令,将一个Cache行清零并分配到Cache中,无需从下级存储取回,直接标记为Dirty。这在初始化大型数据结构时非常有用,可以避免不必要的内存读取。

在结束写入策略的讨论之前,有必要总结一下每个Cache行所包含的全部元数据(metadata)位。一个完整的Cache行包含数据存储区和若干状态/标记位,如图图 5.20所示。

Cache行的元数据字段组成
Cache行的元数据字段组成

各字段的含义如下:

  • Valid (V):1位,表示该Cache行是否包含有效数据。复位后所有行的V位清零。

  • Dirty (D):1位,表示该行是否被修改过(仅在Write Back策略中使用)。

  • Tagtt位(通常30\sim36位),用于命中判断。

  • LRU/替换位:用于替换策略的状态位。Tree-PLRU需要(W1)/W(W-1)/W位/行(共享于组),RRIP需要2位/行。

  • 一致性状态:在多核系统中,每行需要2\sim3位来编码MESI/MOESI协议的状态(M/E/S/I或M/O/E/S/I)。这将在第 11.0 章中详细讨论。

  • ECC:7\sim8位的纠错码(Error Correcting Code),用于检测和纠正SRAM中的软错误(Soft Error)。在先进工艺节点下,由于晶体管尺寸缩小,软错误率增加,ECC变得越来越重要。SECDED(Single Error Correction, Double Error Detection)码是最常用的方案,对64位数据需要8位校验位。

以一个典型的8-way、32 KB、64 B行大小的L1 D-Cache为例,每行的元数据开销为:1+1+36+2+2+8=501 + 1 + 36 + 2 + 2 + 8 = 50位,而数据为512位。因此元数据约占总存储的50/(50+512)8.9%50 / (50 + 512) \approx 8.9\%。对于更大的Cache(如L3),Tag位数较少(因为地址位数相对于Cache容量不变,但Index位数更多),元数据的比例更低。

关于ECC保护值得多说几句。SRAM中的软错误(Soft Error)是由宇宙射线或热中子撞击硅基材料导致的瞬态位翻转。在先进工艺节点下(5 nm及以下),由于晶体管的栅极电容和节点电荷量不断缩小,抵抗外部辐射干扰的能力下降,软错误率(SER,Soft Error Rate)呈上升趋势。对于一个32 MB的LLC,在海平面高度的典型环境下,预计每10410510^4\sim10^5小时可能发生一次软错误。在航空电子或太空应用中,软错误率更高数个数量级。

为了保证数据可靠性,现代处理器的所有Cache层都采用了ECC保护。L1 Cache通常使用奇偶校验(Parity)——每字节1位奇偶位,可以检测单位错误但不能纠正。L2和L3 Cache则使用SECDED(Single Error Correction, Double Error Detection)ECC——对每64位数据增加8位校验码,可以纠正单位错误并检测双位错误。当L1 Cache检测到奇偶校验错误时,通常会从L2重新取回正确数据(因为L2有ECC纠错能力);当L2/L3检测到可纠正的单位错误时,自动纠正并继续;检测到不可纠正的双位错误时,触发机器检查异常(Machine Check Exception)。

在面向2030年代的2 nm和1.4 nm工艺下,软错误问题将更加严峻,可能需要更强的保护方案(如SEC-DED-DAEC或Chipkill级别的ECC)。这些内容超出了本章的范围,但读者应当意识到ECC是现代Cache设计中不可省略的组成部分。

Cache的替换策略

当一个Cache缺失发生且需要将新的Cache行放入一个已满的组中时,必须选择该组中的某一路进行驱逐(Eviction)。替换策略(Replacement Policy)决定了驱逐哪一路。理想的替换策略应当驱逐未来最长时间内不会被再次访问的行(即Bélády最优算法,OPT),但这需要预知未来的访问序列,在实际硬件中无法实现。因此需要设计各种启发式的替换策略来近似最优解。

对于直接映射Cache(W=1W = 1),不存在替换选择的问题——唯一的那一行必须被替换。替换策略只在W2W \ge 2的组相联Cache中才有意义。

LRU

LRU(Least Recently Used,最近最少使用)是最直观的替换策略:驱逐最长时间未被访问的那一路。LRU基于时间局部性的假设——最近被访问过的行在未来被再次访问的概率更高,反之,最久未被访问的行在未来被访问的概率最低。

精确LRU的实现——年龄位方式

一种实现LRU的方式是为每一路维护一个年龄计数器(Age Counter)。在WW-way组相联Cache中,每一路的年龄值范围为00W1W-1:年龄为0表示最近刚被访问过(Most Recently Used, MRU),年龄为W1W-1表示最久未被访问(LRU)。年龄值实质上是一个排列(Permutation)——组中WW个路的年龄值是00W1W-1的一个排列。

更新规则如下:当Way kk被访问(命中或新填入)时:

  1. 记录Way kk当前的年龄值aka_k

  2. 将Way kk的年龄设置为0(MRU)。

  3. 对于所有其他路Way jjjkj \neq k),如果aj<aka_j < a_k(即比Way kk更"年轻"的路),将其年龄加1(变得更"老"了)。

  4. 年龄值ajaka_j \ge a_k的路不受影响。

这种方式需要每路log2W\lceil \log_2 W \rceil位的年龄计数器。对于8-way Cache,每路需要3位,每组需要8×3=248 \times 3 = 24位的LRU状态。此外,更新逻辑需要读取所有路的年龄值,与被访问路的年龄值进行比较,然后有条件地递增,硬件复杂度为O(W)O(W)个比较器和O(W)O(W)个条件加法器。

精确LRU的实现——年龄矩阵方式

另一种精确LRU的实现是使用年龄矩阵(Age Matrix),也称为NRU矩阵。对于WW-way Cache,年龄矩阵是一个W×WW \times W的方阵MM,但由于对角线元素无意义且矩阵关于对角线反对称(M[i][j]=M[j][i]M[i][j] = \overline{M[j][i]}iji \neq j),只需要存储上三角部分,即W(W1)/2W(W-1)/2位。M[i][j]=1M[i][j] = 1i<ji < j)表示Way ii的最近一次访问比Way jj更晚(更年轻)。

更新规则:当Way kk被访问时:

  1. 将第kk行的所有位设置为1:M[k][j]1M[k][j] \leftarrow 1,对所有jkj \neq k

  2. 将第kk列的所有位设置为0:M[i][k]0M[i][k] \leftarrow 0,对所有iki \neq k

这两步操作使得Way kk成为MRU——它比所有其他路都"更新"。

查找LRU路:LRU路是矩阵中整行为0的那一路,即对于Way kk,如果M[k][j]=0M[k][j] = 0对所有jj成立,则Way kk是LRU。等价地,LRU路的行向量为全0。

年龄矩阵的更新逻辑非常简单——只需要对一行置1和一列置0,这可以在一个时钟周期内通过简单的组合逻辑完成,不需要比较器和条件加法器。这是年龄矩阵方式相比年龄位方式的主要优势。

年龄矩阵需要W(W1)/2W(W-1)/2位存储空间。对于4-way Cache,需要4×3/2=64 \times 3 / 2 = 6位/组;对于8-way Cache,需要8×7/2=288 \times 7 / 2 = 28位/组。

以4-way Cache为例,年龄矩阵为4×44 \times 4,存储上三角6位:M[0][1]M[0][1]M[0][2]M[0][2]M[0][3]M[0][3]M[1][2]M[1][2]M[1][3]M[1][3]M[2][3]M[2][3]。假设访问顺序为Way 2, Way 0, Way 3, Way 1。

初始状态(全0,所有行等价):

M=(000100110111)M = \begin{pmatrix} - & 0 & 0 & 0 \\ 1 & - & 0 & 0 \\ 1 & 1 & - & 0 \\ 1 & 1 & 1 & - \end{pmatrix}

(下三角由反对称关系导出:M[i][j]=M[j][i]M[i][j] = \overline{M[j][i]}

行向量为:Way 0 = (0,0,0),Way 1 = (0,0),Way 2 = (0),Way 3 = ()。此时Way 0的行全为0,Way 0是LRU。

访问Way 2:置第2行为1,第2列为0:

M=(000100111110)M = \begin{pmatrix} - & 0 & 0 & 0 \\ 1 & - & 0 & 0 \\ 1 & 1 & - & 1 \\ 1 & 1 & 0 & - \end{pmatrix}

Way 0的行 = (0,0,0),仍为LRU。Way 2的行 = (1,1,1),为MRU。

访问Way 0:置第0行为1,第0列为0:

M=(111000011010)M = \begin{pmatrix} - & 1 & 1 & 1 \\ 0 & - & 0 & 0 \\ 0 & 1 & - & 1 \\ 0 & 1 & 0 & - \end{pmatrix}

现在Way 1的行 = (0,0),Way 1是LRU。Way 0的行 = (1,1,1),为MRU。

如此往复,年龄矩阵始终精确地维护着所有路的访问时间全序。

硬件开销分析

精确LRU的硬件开销随相联度的增加而快速增长。表表 5.10列出了不同相联度下精确LRU的存储开销。

相联度 WW年龄位(位/组)年龄矩阵(位/组)排列数 W!W!理论最少位数
22×1=22 \times 1 = 2121
44×2=84 \times 2 = 86245
88×3=248 \times 3 = 24284032016
1616×4=6416 \times 4 = 641202.09×10132.09 \times 10^{13}44

精确LRU的存储开销(每组)

表中的"理论最少位数"是log2W!\lceil \log_2 W! \rceil,这是编码WW个元素的所有排列所需的最少位数。年龄位方式和年龄矩阵方式都使用了比理论最小值更多的位数(因为它们的编码有冗余),但换来了更简单的更新逻辑。

从表中可以看出,精确LRU对于2-way和4-way Cache是完全可行的,硬件开销微乎其微。对于8-way Cache,28位/组的开销仍可接受(对于64组的Cache,总共64×28=179264 \times 28 = 1792224B\approx 224\,\text{B}),但更新逻辑变得复杂。对于16-way及以上的Cache(如L2/L3 Cache的典型配置),精确LRU的120位/组开销太大,且LRU路的查找逻辑(需要检查16个行向量是否全零)也变得缓慢,通常使用伪LRU或其他近似算法。

伪LRU

伪LRU(Pseudo-LRU, PLRU)是一类用较少的硬件开销来近似LRU行为的替换策略。最常用的是Tree-PLRU(树形伪LRU),它使用一棵二叉树来指示大致的LRU方向。

Tree-PLRU的实现

对于WW-way Cache(WW为2的幂),Tree-PLRU使用一棵有W1W-1个内部节点的完全二叉树,每个内部节点存储1位,总共需要W1W-1位/组。这些位的含义是:指向"较久未被访问"的子树方向。0表示左子树较旧(下次替换向左找),1表示右子树较旧(下次替换向右找)。

图 5.21展示了一个8-way Cache的Tree-PLRU结构。树有7个内部节点(b0b_0b6b_6),叶子节点对应8个Way。

8-way Tree-PLRU的二叉树结构
8-way Tree-PLRU的二叉树结构

工作流程详解

Tree-PLRU的工作分为两个操作:

(1)查找替换目标(Eviction Target):从根节点开始,沿着每个节点指示的"较旧"方向(即位值所指方向)向下遍历到叶子节点。具体来说,在每个节点bib_i处:如果bi=0b_i = 0,向左子树走;如果bi=1b_i = 1,向右子树走。到达的叶子节点就是替换目标。这个过程只需要log2W\log_2 W步(8-way只需3步),可以通过3级MUX在极短的延迟内完成。

(2)更新访问状态(Access Update):当Way kk被访问(命中或新填入)时,将从根到Way kk叶子路径上的所有内部节点更新为指向远离Way kk的方向。例如,如果Way kk在某个节点的左子树中,则将该节点设置为1(指向右子树——因为左子树刚被访问过,较新)。这样,后续的替换搜索就不会指向Way kk所在的方向。

在图图 5.21中,假设初始状态为b0b1b2b3b4b5b6=0000000b_0 b_1 b_2 b_3 b_4 b_5 b_6 = 0000000(所有方向指向左),则替换目标的搜索过程为:b0=0b_0 = 0去左 \to b1=0b_1 = 0去左 \to b3=0b_3 = 0去左 \to 到达Way 0。因此Way 0是替换目标。

如果此时Way 0被新数据填入,则更新从根到Way 0的路径上的位为"远离Way 0"的方向:Way 0在b0b_0的左子树中,设b01b_0 \leftarrow 1;Way 0在b1b_1的左子树中,设b11b_1 \leftarrow 1;Way 0在b3b_3的左子树中,设b31b_3 \leftarrow 1。更新后状态为11001001100100

接下来如果又需要替换,搜索过程为:b0=1b_0 = 1去右 \to b2=0b_2 = 0去左 \to b5=0b_5 = 0去左 \to 到达Way 4。因此Way 4是下一个替换目标。

再假设Way 3被访问(命中),则更新路径为b0b1b4b_0 \to b_1 \to b_4。Way 3在b0b_0的左子树中,设b01b_0 \leftarrow 1(保持不变);Way 3在b1b_1的右子树中,设b10b_1 \leftarrow 0(改为指向左);Way 3在b4b_4的右子树中,设b40b_4 \leftarrow 0(改为指向左)。更新后状态为10000001000000

Tree-PLRU与精确LRU的区别在于:Tree-PLRU只保证选出的替换目标大致位于最久未被访问的路附近,但不保证精确。具体来说,Tree-PLRU可能会选择第二旧或第三旧的路而不是最旧的路。在实际的工作负载中,这种偏差对命中率的影响很小——Tree-PLRU的命中率通常只比精确LRU低0.1%\sim0.5%(在SPEC CPU等基准测试中)。

Tree-PLRU只需要W1W - 1位/组的存储空间(8-way仅需7位,而精确LRU的年龄矩阵需要28位),且更新逻辑简单(只需设置log2W=3\log_2 W = 3个位),可以在1个周期内完成。因此Tree-PLRU被广泛用于8-way及以上的Cache中。Intel的大多数处理器(从Core 2到Alder Lake)的L1 D-Cache都采用Tree-PLRU替换策略。

随机替换

随机替换(Random Replacement)策略在需要替换时随机选择一路进行驱逐。随机替换的优点是实现极为简单——只需要一个LFSR(Linear Feedback Shift Register,线性反馈移位寄存器)来产生伪随机数即可,不需要任何状态存储和更新逻辑。

一个mm位的LFSR可以产生周期为2m12^m - 1的伪随机序列。对于WW-way Cache,需要一个log2W\lceil \log_2 W \rceil位的LFSR(严格来说,需要截断到[0,W)[0, W)范围,但如果WW是2的幂则直接使用LFSR的低位即可)。LFSR的更新逻辑只需要一个异或门和一个移位操作,硬件开销几乎可以忽略不计。而且,LFSR是一个全局的硬件(所有组共用一个),不需要为每个组维护独立的状态。

随机替换在命中率方面通常略低于LRU或PLRU(大约低1%\sim3%),但在某些病态(Pathological)的访问模式下,随机替换反而可能优于LRU。考虑以下场景:程序循环访问一个包含W+1W+1个Cache行的工作集(即恰好比一组的容量大1行)。在这种情况下,LRU策略会在每次迭代中系统性地驱逐即将在下一次迭代中被需要的行,导致每次访问都缺失——命中率为0%。这就是所谓的Thrashing(颠簸)现象。而随机替换在每次替换时有1/W1/W的概率保留即将被需要的行,因此平均命中率约为1/(W+1)1/(W+1),虽然仍然很低,但比LRU的0%好得多。

ARM的Cortex-A系列处理器广泛使用随机替换策略(可通过CP15配置寄存器选择LRU或Random),这在很大程度上是因为随机替换在嵌入式和移动场景中提供了良好的性能/功耗/面积平衡。在这些场景中,简单性和可预测性(避免Thrashing)比追求最后1%的命中率更重要。

从另一个角度来看,随机替换的一个独特优势是它的行为完全不依赖于访问模式——没有任何一种访问序列能使随机替换的性能系统性地恶化(除了命中率本身偏低这一固有缺点)。相比之下,LRU和PLRU都可能在特定的病态访问模式下遭受严重的Thrashing。这种鲁棒性在安全关键系统(如汽车电子、航空电子)中是一个有价值的属性,因为系统的行为需要在所有输入条件下都是可预测的。

此外,随机替换还有一个在安全领域的优势:它使得基于Cache替换行为的侧信道攻击更加困难,因为攻击者无法通过精心构造的访问序列来推断出特定的替换决策。

RRIP族替换策略

传统的LRU策略假设最近被访问的数据在未来最有可能被再次访问。然而,这个假设并非总是成立。考虑一个扫描(Scan/Streaming)模式的访问序列:程序顺序地遍历一个远大于Cache容量的数组,每个元素只访问一次。在这种情况下,LRU会将所有的Cache行都替换为扫描数据,而这些扫描数据永远不会被再次使用——LRU策略反而将有用的数据(可能具有较好的时间局部性,如热点数据结构)驱逐了。这个问题在Last-Level Cache(LLC)中尤为严重,因为LLC被多个核共享,不同核可能同时运行不同类型的工作负载。

RRIP(Re-Reference Interval Prediction)族替换策略由Intel的Aamer Jaleel等人在2010年的ISCA会议上提出,旨在通过预测每个Cache行的重用距离(Re-Reference Interval)来做出更好的替换决策。RRIP为每个Cache行维护一个MM位的RRPV(Re-Reference Prediction Value)值,范围为002M12^M - 1。RRPV值越大,表示预测该行在未来越久才会被再次访问(即越应当被优先替换)。实际实现中通常使用M=2M = 2(即2-bit RRIP),需要每行2位的额外存储。

对于M=2M = 2的RRIP,四个RRPV状态的含义如下:

  • RRPV=0\text{RRPV} = 0:Near-immediate re-reference——预测该行即将被重用,应当被高度保护。

  • RRPV=1\text{RRPV} = 1:Intermediate re-reference——预测该行将在中等时间内被重用。

  • RRPV=2\text{RRPV} = 2:Long re-reference——预测该行在较长时间后才会被重用(SRRIP中新行的默认值)。

  • RRPV=3\text{RRPV} = 3:Distant re-reference——预测该行在很长时间内不会被重用,是替换的首选候选。

图 5.22展示了2-bit RRIP中每个Cache行的RRPV状态转移图。

2-bit RRIP的RRPV状态转移图
2-bit RRIP的RRPV状态转移图

SRRIP(Static RRIP)

SRRIP(Static Re-Reference Interval Prediction)是RRIP族中最基本的策略。其规则如下:

  • 插入(Insert):新行插入时,RRPV设置为2M22^M - 2(即"长重用距离"预测,对于M=2M=2RRPV=2\text{RRPV}=2)。这样新插入的行不会立即成为替换候选(RRPV3\text{RRPV}\neq 3),但也不会被过度保护。

  • 命中(Hit):命中时,将该行的RRPV设置为0("即将重用"预测),这类似于LRU中将命中行移到MRU位置。

  • 替换(Evict):寻找RRPV=2M1\text{RRPV} = 2^M - 1(即3)的行作为替换目标。如果没有找到,则将组中所有行的RRPV加1(即所有行都变得"更老"了),然后重新搜索。重复此过程直到找到一个RRPV=3\text{RRPV} = 3的行。由于每次所有行的RRPV都加1,这个搜索过程最多需要2M12^M - 1次递增(对于M=2M=2最多3次)就一定能找到一个RRPV为3的行。

SRRIP与LRU的关键区别在于插入位置:LRU将新行放在MRU位置(最受保护),而SRRIP将新行放在RRPV=2\text{RRPV}=2的位置(只受适度保护)。这意味着扫描数据在插入后很快就会被替换(因为其RRPV从2很快被增加到3),而之前的有用数据(RRPV可能为0或1)得以保留。

考虑一个4-way Cache的某个组,使用2-bit SRRIP。初始状态下,4路的RRPV为:

Way 0Way 1Way 2Way 3
初始0120

假设此时发生一次缺失,需要插入新行。查找RRPV = 3的行:没有找到。将所有行的RRPV加1:

Way 0Way 1Way 2Way 3
+1后1231

现在Way 2的RRPV = 3,选择Way 2作为替换目标。新行插入Way 2,RRPV设为2:

Way 0Way 1Way 2Way 3
插入后1221

如果接下来Way 1命中,则将Way 1的RRPV设为0:

Way 0Way 1Way 2Way 3
命中后1021

可以看到,RRPV值形成了一种优先级排序:0表示最不应被替换,3表示最应被替换。与LRU类似,但SRRIP通过在插入时设置RRPV = 2(而非LRU的MRU位置0),使新行不会过度挤占老行的位置。

SRRIP的硬件实现非常简单。替换时,搜索RRPV = 3的操作可以通过一个WW路的比较器阵列在一个周期内完成。如果没找到,所有行的RRPV加1可以通过WW个2位加法器并行完成。在最坏情况下需要3次迭代(搜索 \to 加1 \to 搜索 \to 加1 \to 搜索 \to 加1 \to 搜索),但在实际工作负载中,通常在1\sim2次迭代内就能找到替换目标。如果对延迟敏感,可以将搜索和加1的操作流水化,或使用一个优先编码器直接找出RRPV最大的行。

BRRIP(Bimodal RRIP)

BRRIP在SRRIP的基础上做了一个微小但重要的改变:大部分新行插入时RRPV设置为2M12^M - 1(即3,distant),只有偶尔(以一个很低的概率ϵ\epsilon,典型值为1/321/32)才设置为2M22^M - 2(即2,long)。这意味着绝大多数新插入的行立即成为替换候选,从而极其有效地抵抗了扫描模式的污染——扫描数据在插入后几乎马上就会被替换,而之前的有用数据(RRPV较低)完全不受影响。

BRRIP在高度扫描化的工作负载中表现出色(接近OPT),但对于局部性良好的纯重用工作负载,BRRIP可能不如SRRIP,因为新的有用数据也会被过于激进地替换掉——绝大多数新行以RRPV=3插入,意味着它们在下一次替换时就会被驱逐,即使这些行本应该被保留。

从直觉上看,SRRIP适合"大部分新数据有用"的工作负载(如频繁重用的数据库索引查询),BRRIP适合"大部分新数据无用"的工作负载(如大规模流式数据处理)。既然事先不知道工作负载的特征,而且工作负载的特征可能随时间动态变化,就需要一种机制来在运行时自适应地在两者之间动态切换—— 这就是DRRIP的动机。

DRRIP(Dynamic RRIP)

DRRIP通过Set Dueling(组对决)技术动态地在SRRIP和BRRIP之间选择,兼顾两者的优势。Set Dueling的核心思想是:用少量Cache组作为"探针"来评估两种策略的效果,然后让大部分组跟随表现更好的策略。具体做法如下:

  1. 将Cache中的少量组(通常32\sim64组,约占总组数的1%\sim2%)划分为Dedicated组:一半(如16组)专用于SRRIP策略,称为SRRIP Dedicated组;另一半(16组)专用于BRRIP策略,称为BRRIP Dedicated组。这些组的选择通常使用散列函数来分散在整个Cache中,避免局部性偏差。

  2. 使用一个全局的Policy Selection Counter(策略选择计数器,PSC),通常为10位饱和计数器。SRRIP Dedicated组每发生一次缺失,PSC加1;BRRIP Dedicated组每发生一次缺失,PSC减1。

  3. 其余的组(占绝大多数)称为Follower组,根据PSC的MSB(最高有效位)选择策略:MSB为1时使用BRRIP,MSB为0时使用SRRIP。

DRRIP只需要极少的额外硬件(32个Dedicated组 + 1个10位计数器),就可以自适应地选择最合适的插入策略。在SPEC CPU等基准测试中,DRRIP在混合工作负载下几乎总是选择更好的策略,其性能接近SRRIP和BRRIP中较好的那个,且通常优于纯LRU策略2%\sim5%。

DRRIP只需要极少的额外硬件开销:32个Dedicated组(约占总组数的1%\sim2%,对缺失率的影响微乎其微)加上1个10位饱和计数器。Set Dueling的思想具有很强的通用性——它可以用于在任意两种Cache策略之间进行运行时选择,不仅限于替换策略。例如,同样的技术也可以用于动态选择预取策略、Cache旁路(Bypass)策略等。Set Dueling的发明者Moinuddin Qureshi等人在2007年的ISCA论文中首次提出了这一技术,后来被广泛应用于各种Cache管理机制中。

Intel从Ivy Bridge(2012年)开始在LLC中采用了RRIP的变体。后续的Haswell、Broadwell、Skylake等架构持续改进了LLC的替换策略,融入了更多的自适应和感知能力。

SHiP

SHiP(Signature-based Hit Predictor)由Wu等人于2011年的MICRO会议上提出,是对RRIP的进一步改进。SHiP的核心思想是:不同类型的内存访问具有不同的重用特征,应当为不同的访问签名(Signature)赋予不同的初始RRPV值。

动机

SRRIP和BRRIP对所有新插入的行使用相同的初始RRPV值,这是一种"一刀切"的方式。但在实际程序中,不同指令产生的访问具有截然不同的重用行为:

  • 一个在循环中反复访问同一小数据集的Load指令,其访问的数据具有良好的时间局部性——应当赋予低RRPV(保护)。

  • 一个顺序扫描大数组的Load指令,其访问的数据不会被重用——应当赋予高RRPV(尽快替换)。

  • 一个执行DMA或初始化大缓冲区的Store指令,其写入的数据可能只被读一次然后丢弃——应当赋予高RRPV。

SHiP通过将每次访问与一个签名关联,学习每种签名的重用行为,然后在插入时根据签名决定初始RRPV。

签名的选择

最常用且最有效的签名是PC签名(程序计数器签名)——即执行该Load/Store指令的PC值的低kk位(通常k=14k = 14位)。PC签名的直觉是:相同PC处的指令通常执行相同类型的操作(如循环中的重复访问vs.流式扫描),因此具有相似的重用特征。

其他可选的签名包括:

  • 内存区域签名:目标地址的高位,用于区分不同的数据结构或内存区域。

  • 指令类型签名:根据指令类型(Load/Store/Prefetch)和访问模式(Stride/Random)生成签名。

硬件实现

SHiP维护一个全局的SHCT(Signature History Counter Table),以签名为索引,每项是一个nn位饱和计数器(通常n=3n = 3)。SHCT的大小为2k2^k项(如214=163842^{14} = 16384项),每项3位,总容量为16384×3/8=6KB16384 \times 3 / 8 = 6\,\text{KB}。计数器值反映了该签名对应的访问数据被重用的倾向:计数器值高表示该签名的数据通常会被重用;计数器值低(尤其是0)表示该签名的数据通常不会被重用。

此外,每个Cache行需要增加两个字段:

  • Signature字段kk位(如14位),记录该行是由哪个签名产生的访问所带入的。

  • Outcome位:1位,记录该行在Cache中是否曾被命中过。插入时清零,命中时置位。

SHiP的工作流程如下:

  1. 插入(Insert):当新行插入Cache时,记录其签名(PC的低14位)到该行的Signature字段,清除Outcome位为0。查询SHCT中该签名对应的计数器值:

    • 如果计数器值 =0= 0:设RRPV=2M1\text{RRPV} = 2^M - 1(distant),预测不会被重用。

    • 如果计数器值 >0> 0:设RRPV=2M2\text{RRPV} = 2^M - 2(long),给予适度保护。

  2. 命中(Hit):当Cache行命中时:

    • 设该行的RRPV=0\text{RRPV} = 0(near-immediate)。

    • 设该行的Outcome位为1。

    • 将SHCT中该行签名对应的计数器递增(因为确认了该签名的数据被重用了)。

  3. 驱逐(Evict):当一行被驱逐时,检查其Outcome位:

    • 如果Outcome =0= 0(该行从插入到驱逐从未被命中过),说明该签名的数据确实不会被重用。将SHCT中该签名对应的计数器递减。

    • 如果Outcome =1= 1(该行曾被命中过),不更新SHCT(因为命中时已经递增了)。

通过这种反馈机制,SHCT逐渐学习到每种签名的重用行为:经常产生重用的签名的计数器值保持在高位,其带入的新行获得较低的RRPV(受保护);很少产生重用的签名(如扫描指令的PC)的计数器值降为0,其带入的新行获得RRPV=3\text{RRPV} = 3(立即成为替换候选)。

效果与开销

SHiP的硬件开销非常小:每行增加14位签名 + 1位Outcome = 15位,再加上一个全局的SHCT(6 KB)。这些开销相对于Cache本身的容量(通常数MB的LLC)微乎其微,但带来的命中率提升可达2%\sim5%(在SPEC CPU基准测试中相对于SRRIP)。

SHiP的变体SHiP++进一步改进了签名的计算方式(使用PC\oplus内存地址的哈希作为签名),在更多的工作负载下展现出优秀的性能。SHiP及其变体已被Intel在Skylake(2015年)及后续架构的LLC中采用。

表 5.11对本节讨论的各种替换策略进行了全面比较。

案例研究 3 — 真实处理器中的替换策略
  • Intel Skylake/Alder Lake:L1 D-Cache使用Tree-PLRU;L2 Cache使用Tree-PLRU或变体;L3(LLC)使用Adaptive Replacement策略(基于RRIP/SHiP的变体),具有抗扫描能力。

  • AMD Zen 3/Zen 4:L1 D-Cache使用PLRU变体;L2 Cache使用LRU或PLRU;L3 Cache(V-Cache版本可达96 MB)使用自适应替换策略。

  • Apple M系列:L1 D-Cache使用PLRU;苹果的SLC(System Level Cache)使用专有的替换算法,针对异构大小核的共享场景进行了优化。

  • ARM Cortex-X系列:支持通过配置选择随机替换或PLRU。在性能敏感的场景下通常配置为PLRU。

  • RISC-V香山:L1 D-Cache和L2 Cache使用PLRU;L3 Cache的替换策略可配置。

设计提示

Cache替换策略的选择需要在命中率提升和硬件开销之间权衡。对于L1 Cache,由于相联度通常为8\sim12-way,容量较小(32\sim128 KB),替换策略对命中率的影响相对较小(因为L1的命中率主要由容量和冲突缺失决定),因此Tree-PLRU是最常见的选择——它只需要W1W-1位/组,更新逻辑简单,可以在1个周期内完成。

对于LLC(通常16\sim20-way,容量数MB到数十MB),替换策略的影响更大,因为LLC需要在多个核的工作集之间分配有限的容量。LRU在LLC中表现不佳(容易被扫描流量污染),RRIP和SHiP等感知重用距离的策略可以显著提高LLC的有效利用率。DRRIP和SHiP的硬件开销极小(每行仅增加2\sim15位),是LLC替换策略的最佳选择。

随机替换虽然简单,但在绝大多数场景下命中率不如PLRU,只在某些特定场合才考虑使用:极端面积/功耗约束的嵌入式处理器,或作为"保底"策略与其他策略配合使用(如DRRIP中的BRRIP组件就接近于随机插入)。

Cache访问流水线

L1 Cache的访问延迟直接决定了Load指令的延迟,而Load延迟是影响乱序处理器性能的最关键因素之一——它决定了数据依赖链的最短长度。在一条Load指令取到数据之前,所有依赖于这条Load结果的后续指令都无法执行。因此,L1 D-Cache的访问需要尽可能少的流水线级数。

然而,Cache访问涉及多个复杂的硬件操作(地址计算、SRAM读取、Tag比较、Way选择、数据对齐),将所有操作压缩在一个时钟周期内完成(在高频设计中)可能非常困难。不同的处理器根据其设计目标(频率 vs. 延迟)选择了不同的流水线级数。

1级Cache访问

1级访问方案中,Cache访问的所有操作在一个时钟周期内完成:

1级Cache访问的时序
1级Cache访问的时序

1级访问的关键路径包括:地址生成单元(AGU)延迟 + SRAM行解码延迟 + SRAM位线延迟 + 灵敏放大延迟 + Tag比较延迟 + Way MUX延迟 + 数据对齐延迟。在高频设计(如5 GHz,时钟周期200 ps)中,将所有这些操作压缩在一个周期内需要非常小的Cache容量(通常不超过16 KB)和低相联度(通常为直接映射或2-way),否则SRAM和MUX的延迟将超出时钟周期的限制。

1级访问的优点是Load-to-Use延迟最低(只有1个周期),但约束非常严格。在现代高性能处理器中,1级访问已经很少用于L1 D-Cache(因为容量和相联度的要求远超1级访问所能支持的范围),但某些处理器的L1 I-Cache(访问模式更简单、可以使用直接映射或低相联度)可能采用1级访问。

2级Cache访问

2级访问是最常见的L1 D-Cache流水线方案。Cache访问分布在两个时钟周期中:

2级Cache访问的时序
2级Cache访问的时序
  • 第1级(周期NN:地址计算(AGU将基址寄存器与偏移量相加),然后用Index同时访问所有路的Tag SRAM和Data SRAM。SRAM的读取操作是第1级的主要延迟来源。在周期NN结束时,所有路的Tag值和数据被锁存到流水线寄存器中。

  • 第2级(周期N+1N+1:将锁存的Tag值与地址的Tag字段(来自TLB转换后的物理地址)进行比较,确定命中路号。使用Way MUX从WW路数据中选出命中路的数据。对选出的数据进行字节对齐(根据Offset和访问宽度,从Cache行数据中提取目标字节/半字/字/双字),输出最终结果。

2级访问的Load-to-Use延迟为2个周期——依赖于Load结果的后续指令最早可以在Load发出的2个周期后开始执行。这是大多数中高频处理器(3 GHz\sim5 GHz)的L1 D-Cache所采用的方案。

3级Cache访问

在最高频率的设计中(如Intel Skylake及后续架构的4.5 GHz\sim5.5 GHz核心),即使是2级访问也可能时序紧张,需要将Cache访问进一步拆分为3级

3级Cache访问的时序(如Intel Skylake L1D)
3级Cache访问的时序(如Intel Skylake L1D)

3级访问的时序分解如下:

  • 第1级:地址计算(AGU) + SRAM行解码器启动。行解码器将Index翻译为字线选择信号,但字线驱动和位线放电尚未完成。

  • 第2级:字线驱动完成 \to 位线放电 \to 灵敏放大器检测并输出数据 \to Tag比较开始(并行进行:Data SRAM的数据读出和Tag SRAM的比较可以部分重叠)。

  • 第3级:Tag比较完成,确定命中路号 \to Way MUX选择命中路的数据 \to 数据对齐(字节/半字/字选择和符号扩展) \to 输出到旁路网络。

Intel Skylake的L1 D-Cache为32 KB/8-way,其Load延迟为5个周期(包括2个周期的地址生成和调度)。后续的Golden Cove架构将L1D扩大到48 KB/12-way,Load延迟仍为5个周期,但内部的SRAM读取可能使用了更先进的电路技术来维持时序。

不同流水线级数的权衡

表 5.12总结了不同Cache访问流水线级数的权衡。

级数Load-to-Use最大Cache容量最大频率代表处理器
1级1 cycle\sim16 KB\sim2 GHz早期ARM核心
2级2 cycle\sim32 KB\sim4 GHzARM Cortex-A77
3级3 cycle\sim48 KB\sim5.5 GHzIntel Skylake
4级4 cycle\sim128 KB\sim3.5 GHzApple M4 P-core

Cache访问流水线级数的权衡

增加流水线级数意味着更高的Load-to-Use延迟,这会拉长数据依赖链的长度,对指针追踪(Pointer Chasing)类的工作负载尤其不利——每次链表遍历的p = p->next操作都需要等待上一次Load完成才能发出下一次Load,延迟完全串行累加。在3级访问下,遍历一个包含100个节点的链表需要至少300个周期,而2级访问只需要200个周期。

然而,更多的流水线级数允许更大的Cache容量和更高的工作频率。更大的Cache意味着更低的缺失率,而Cache缺失的代价(到L2的延迟约为10\sim14个周期)远大于1\sim2个周期的Load延迟增加。因此,是否值得增加一级流水线来换取更大的Cache容量,需要根据目标工作负载的特征来权衡。

性能分析 3 — Load延迟对IPC的定量影响

可以通过一个简单的模型来估算Load-to-Use延迟对IPC的影响。假设:

  • 处理器宽度为6-wide,理想IPC为6

  • Load指令占全部指令的25%

  • 其中30%的Load处于关键依赖链上(即有后续指令直接依赖其结果)

  • L1 D-Cache命中率为97%

在Load-to-Use延迟从3个周期增加到4个周期时,关键路径上的每个Load增加1个周期的延迟。平均每条指令中,关键路径Load的贡献为0.25×0.30=7.5%0.25 \times 0.30 = 7.5\%的指令。这些Load每个增加1个周期,但由于乱序执行可以部分掩盖这个延迟(假设掩盖率为60%),实际影响为:

ΔIPC0.075×0.401+0.075×0.40×60.17\Delta\text{IPC} \approx -\frac{0.075 \times 0.40}{1 + 0.075 \times 0.40} \times 6 \approx -0.17

即IPC降低约0.17,相对降幅约2.8%。这个估算与实际的微架构模拟结果大致吻合:Intel从Haswell(4周期Load延迟)到Skylake(5周期Load延迟,但Cache增大到32 KB/8-way)的改变中,增大的Cache容量带来的缺失率降低足以弥补Load延迟增加的性能损失。

Cache行跨越访问

当一次Load或Store操作的地址不是其访问宽度的自然对齐地址时,可能出现Cache行跨越(Cache Line Crossing)的情况——请求的数据跨越了两个相邻的Cache行。例如,一个4字节Load的地址为0x...3E(相对于64 B行的Offset为62),需要读取字节62\sim65,但字节62\sim63在当前Cache行中,字节64\sim65在下一个Cache行中。

Cache行跨越需要特殊处理,因为两部分数据位于不同的Cache行中,可能需要两次独立的Cache访问:

  • 拆分为两次访问:硬件将跨越的Load/Store拆分为两次独立的Cache访问——一次访问当前行的尾部字节,一次访问下一行的头部字节。两次访问的结果在寄存器文件写回之前被合并。这种方式的延迟约为2倍的正常Load延迟,且占用两个Load端口的发射带宽。

  • 专用合并缓冲:部分处理器使用专用的合并缓冲(Split Buffer)来处理跨越访问。第一次访问的部分结果暂存在缓冲中,第二次访问完成后与缓冲中的数据合并输出。

Cache行跨越访问在实际程序中的频率取决于编译器的对齐策略和程序的数据结构布局。对于x86处理器(支持非对齐访问),跨越访问的频率约为1%\sim3%。对于RISC-V和ARM处理器,如果硬件支持非对齐访问(如RISC-V的标准扩展),跨越访问的频率类似;如果要求自然对齐(非对齐访问触发异常),则由编译器保证不会出现跨越访问。

在Cache行跨越访问中,如果恰好两个Cache行一个命中一个缺失,则处理更加复杂——命中部分的数据可以立即返回,缺失部分需要等待缺失处理完成。部分处理器支持这种部分命中(Partial Hit)的优化,使命中部分的数据可以先被使用。

更严重的情况是页面跨越(Page Crossing)——如果跨越的两个Cache行恰好位于不同的虚拟页面中,则除了两次Cache访问外,还需要两次TLB查找(因为两个页面的虚拟-物理映射可能不同)。页面跨越的概率远低于Cache行跨越,但处理开销更大。在大多数处理器中,页面跨越的Load/Store会被拆分为两条微操作(Micro-Op),分别在流水线中独立执行。

为了减少Cache行跨越和页面跨越的发生频率,编译器和运行时库通常会确保关键数据结构(如数组、栈帧)按Cache行边界或页面边界对齐。C/C++中的__attribute__((aligned(64)))和C11的_Alignas(64)就是用于此目的的语言机制。RISC-V和ARM的ABI规范也通常要求栈指针保持16字节对齐,以减少栈变量的跨越访问。对于性能关键的数据结构(如哈希表的桶数组、B+树的节点),程序员可以使用显式的Cache行对齐来完全消除跨越访问,从而获得确定性的Cache访问延迟。

设计提示

对于L1 D-Cache的流水线级数选择,一个有用的经验法则是:如果增加一级流水线可以将Cache容量扩大一倍(从而将缺失率降低约30%\sim40%),那么这种权衡通常是值得的。因为每次L1缺失都要付出至少10个周期的代价(到L2的延迟),而每次Load命中只额外付出1个周期的代价。设缺失率从mm降低到0.65m0.65m,则性能变化为:

ΔAMAT=10.35m×tL210.35m×12\Delta\text{AMAT} = 1 - 0.35m \times t_{\text{L2}} \approx 1 - 0.35m \times 12

m>1/(0.35×12)0.24m > 1/(0.35 \times 12) \approx 0.24(即缺失率高于24%)时,增加一级流水线是值得的。对于大多数实际工作负载(L1 D-Cache缺失率通常为2%\sim10%),增加一级流水线同时能将容量扩大到满足工作集需求的临界点时,权衡才有意义。

一个重要的微架构优化是推测性Load唤醒(Speculative Load Wakeup):在多级Cache访问流水线中,处理器可以在Cache命中确认之前就推测性地唤醒依赖于Load结果的后续指令。具体来说,在2级访问的第1级末尾(SRAM数据读出但Tag比较尚未完成时),如果处理器推测Load会命中(L1命中率通常高于95%),就提前将依赖指令调度执行。如果推测正确(Cache确实命中),依赖指令获得了1个周期的延迟节省;如果推测错误(Cache缺失),需要取消已调度的依赖指令并重新调度,但由于L1缺失率很低,推测成功的概率远高于失败的概率。

Intel和AMD的处理器都广泛使用了这种推测性唤醒技术。在Intel的架构中,虽然L1 D-Cache的物理访问需要5个周期,但由于推测性唤醒,有效的Load-to-Use延迟(对于L1命中的Load)可以降低到4个周期。

综合设计实例

前面各节分别讨论了Cache的组成方式、SRAM实现、多端口设计、VIPT约束、写策略、替换策略和访问流水线等方面。本节将这些设计维度综合在一起,通过一个完整的设计实例来展示各种设计选择如何相互影响和约束。

目标规格

假设我们要为一个2030年代的6-wide乱序超标量处理器设计L1 D-Cache,目标规格如下:

  • 工作频率:5 GHz(时钟周期200 ps)

  • 工艺节点:3 nm

  • 端口需求:2个Load + 1个Store(每周期最多3次访问)

  • 页面大小:4 KB(标准x86/RISC-V页面)

  • 物理地址宽度:52位

  • Load-to-Use延迟目标:\le4个周期

容量与相联度的确定

首先确定Cache的容量和相联度。VIPT约束C/W4KBC/W \le 4\,\text{KB}限制了设计空间:

配置C/WC/W满足VIPT?权衡
32 KB, 8-way4 KB保守,缺失率偏高
48 KB, 12-way4 KB12-way MUX稍复杂
64 KB, 16-way4 KB16-way MUX延迟大
64 KB, 8-way8 KB需要硬件Anti-Aliasing

我们选择48 KB/12-way配置(参照Intel Golden Cove的设计),因为它在不违反VIPT约束的情况下提供了比32 KB更大的容量,且12-way的硬件复杂度在3 nm工艺下可以接受。

地址位划分

  • 组数 = 48×1024/(64×12)=6448 \times 1024 / (64 \times 12) = 64

  • Offset = 6位(a5a0a_5 \cdots a_0

  • Index = 6位(a11a6a_{11} \cdots a_6

  • Tag = 5266=4052 - 6 - 6 = 40位(a51a12a_{51} \cdots a_{12}

  • Index + Offset = 12位 = 4 KB页面的页内偏移,满足VIPT约束

SRAM阵列设计

Tag SRAM:每项包含40位Tag + 1位Valid + 1位Dirty + 2位一致性状态(MESI) + 2位PLRU(Tree-PLRU,11位/组共享,约每行0.9位) = 约47位/项。总容量 = 64×47×12=3609664 \times 47 \times 12 = 360964.4KB\approx 4.4\,\text{KB}

Data SRAM:每路64×512=3276864 \times 512 = 32768位 = 4 KB。采用8个Bank的Multi-banking设计,每个Bank每路存储64×64=409664 \times 64 = 4096位 = 512 B。每个Bank为单端口SRAM。

多端口实现

采用8-Bank Multi-banking方案:

  • 用Offset的高3位(a5a4a3a_5 a_4 a_3)选择Bank。每个Bank存储Cache行中的8字节。

  • 2个Load端口和1个Store端口共享8个Bank。

  • Bank冲突概率:2个Load同时访问时,冲突概率为1/8=12.5%1/8 = 12.5\%。加上Store端口后,1(8×7×6)/8334%1 - (8 \times 7 \times 6)/8^3 \approx 34\%的周期可能出现至少一次冲突,但大多数冲突可以通过推迟一个周期解决。

  • Tag SRAM不分Bank(因为Tag宽度小,所有12路的Tag可以在一次读取中全部输出)。Tag SRAM需要至少2个读端口(供2个Load使用)和1个写端口——使用标准的2读1写端口SRAM即可(Tag SRAM容量小,多端口的面积开销可接受)。

流水线设计

在5 GHz的时钟频率下,采用3级流水线

  • 第1级(AGU + SRAM解码):地址计算单元将基址和偏移相加,生成48位虚拟地址。用a11a6a_{11} \cdots a_6(Index)启动Tag SRAM和Data SRAM的行解码。同时用VA的高位查询TLB。

  • 第2级(SRAM读取 + Tag比较):Tag SRAM和Data SRAM的位线放电并通过灵敏放大器读出。TLB返回物理Tag。将物理Tag与12路读出的Tag值比较,确定命中路号。

  • 第3级(Way选择 + 数据对齐):12:1 Way MUX根据命中信号选择数据。按Offset对数据进行字节对齐和符号扩展。将结果通过旁路网络送到执行单元。

有效Load-to-Use延迟:物理上为3个周期(加上AGU共4个周期),但通过推测性唤醒(在第2级末尾推测Cache命中),有效延迟可以降低到3个周期。推测错误(L1缺失)时需要1个周期的恢复惩罚。

写策略与替换策略

  • 写策略:Write Back + Write Allocate。Store数据通过Store Buffer提交后写入L1 Cache。被驱逐的Dirty行通过4项Write-Back Buffer异步写回L2。

  • 替换策略:Tree-PLRU。12-way Cache需要121=1112 - 1 = 11位/组的Tree-PLRU状态(注意12不是2的幂,需要使用非完全二叉树的变体或分组PLRU)。总PLRU存储 = 64×11=70464 \times 11 = 70488B\approx 88\,\text{B}

面积与功耗估算

在3 nm工艺下的面积估算:

  • Data SRAM:48KB×8bit/B×0.022μm2/bit0.0086mm248\,\text{KB} \times 8\,\text{bit/B} \times 0.022\,\mu\text{m}^2/\text{bit} \approx 0.0086\,\text{mm}^2(纯位单元)。加上外围电路后约0.015 mm2

  • Tag SRAM:4.4KB×8bit/B×0.0220.0008mm24.4\,\text{KB} \times 8\,\text{bit/B} \times 0.022 \approx 0.0008\,\text{mm}^2(纯位单元)。加上外围和比较器后约0.003 mm2

  • 控制逻辑(仲裁器、Way MUX、对齐电路等):约0.005 mm2

  • 总面积:约0.023 mm2,占典型3 nm高性能核心面积(\sim5 mm2)的约0.5%。

动态功耗方面,L1 D-Cache的读功耗主要来自Data SRAM的位线充放电。在Multi-banking方案下,每次Load只激活1个Bank(而非全部8个Bank),Data SRAM的动态功耗约为非Bank方案的1/81/8。加上Way Prediction(如果采用),Data SRAM的功耗可以进一步降低到只读取1路(而非12路),总功耗降低约12倍。

静态功耗(漏电功耗)方面,在3 nm工艺下,SRAM的漏电流是一个不容忽视的问题。一个48 KB的Data SRAM在0.75 V工作电压下的漏电功耗约为5 mW\sim10 mW(取决于位单元类型和环境温度)。对于移动处理器,在低功耗待机模式下,可以通过电源门控(Power Gating)技术关断不活跃的Cache SRAM阵列的电源,将漏电功耗降低到接近零。但电源门控后SRAM中的数据会丢失,恢复供电后需要重新预热Cache,这会产生一段时间的冷启动缺失。部分处理器通过保留最低限度的供电(Retention Voltage,约0.4 V\sim0.5 V)来在降低漏电的同时保持SRAM数据,但这需要特殊的位单元设计(如高阈值保持管)来保证在低电压下的数据保持能力。

这个综合设计实例展示了现代L1 D-Cache设计中各种约束和选择的相互作用:VIPT约束限制了容量/相联度的组合,频率目标决定了流水线级数,端口需求驱动了Multi-banking的选择,而功耗目标则推动了Way Prediction的采用。一个优秀的Cache微架构师需要在这些相互约束的维度中找到最优的设计点。

本章小结

本章系统介绍了Cache的基本原理,从存储单元的晶体管级实现到系统级的设计权衡,力图提供一个自底向上的完整视图。

SRAM存储基础。从6T SRAM位单元的交叉耦合反相器结构出发,详细分析了读操作中的位线放电与灵敏放大过程、写操作中的存储节点翻转机制,以及读稳定性与写裕度之间的矛盾约束。介绍了8T SRAM位单元如何通过增加独立的读端口来消除读扰动问题,以及SRAM阵列的物理组织(行解码器、位线、灵敏放大器、列MUX等)和延迟分解。这些底层知识是理解Cache延迟和面积特性的基础。

Cache的组成方式。讨论了直接映射、组相联和全相联三种结构,详细分析了Tag/Index/Offset的地址划分方式。通过多个具体配置的计算实例(32 KB/8-way、48 KB/12-way、64 KB/4-way等),展示了地址位的逐步推导过程。对比了并行访问和串行访问两种硬件实现方案的延迟和功耗特性,介绍了Way Prediction技术如何在不牺牲延迟的情况下降低功耗。

VIPT机制与Alias问题。深入分析了VIPT(虚拟索引、物理标记)访问方式的工作原理,解释了为什么VIPT是L1 Cache的主流选择(TLB查找与SRAM读取并行)。详细讨论了当Cache每路大小超过页面大小时出现的Alias问题及其四种解决方案:限制每路大小、使用大页面、页着色和硬件Anti-Aliasing。

多端口Cache实现。系统比较了真正的多端口SRAM、Cache复制和Multi-banking三种多端口实现方案的面积、延迟和复杂度权衡。以AMD Opteron的64 KB L1 D-Cache为案例,详细分析了8-Bank Multi-banking方案的Bank划分方式、Bank冲突概率和仲裁机制。

写入策略与缓冲结构。介绍了Write Back与Write Through、Write Allocate与No-Write Allocate的不同组合及其适用场景。深入分析了Write-Back Buffer的设计和Write Buffer的硬件结构(包括CAM匹配和字节有效位的写合并机制),以及Store Buffer与Write Buffer的层次关系。

Cache行的元数据。介绍了Cache行的完整元数据组成,包括Valid、Dirty、Tag、LRU状态、一致性状态和ECC校验码等字段,并讨论了先进工艺下软错误保护的重要性。

替换策略。从精确LRU(年龄位和年龄矩阵两种实现)到Tree-PLRU、随机替换,再到面向Scan-Resistant的RRIP族策略(SRRIP/BRRIP/DRRIP)和利用PC签名的SHiP策略,系统展示了替换策略从简单到复杂的演进路线。Set Dueling作为在两种策略间动态选择的通用技术,具有广泛的适用性。

Cache访问流水线。分析了1级、2级和3级Cache访问流水线的时序分解和权衡,包括每种方案所能支持的最大Cache容量和工作频率。介绍了推测性Load唤醒技术如何在不增加物理流水线级数的情况下降低有效的Load-to-Use延迟。

综合设计实例。通过一个面向2030年代的48 KB/12-way L1 D-Cache完整设计,展示了VIPT约束、Multi-banking、流水线级数、写策略和替换策略等设计维度如何相互影响和约束,以及如何在这些维度中找到最优设计点。

这些基本原理是理解后续章节内容的基础。第 6.0 章将深入讨论虚拟地址与Cache的交互(VIPT/PIPT/VIVT等),分析不同地址转换方式对Cache设计的约束。第 7.0 章将介绍非阻塞Cache(Non-Blocking Cache)和MSHR(Miss Status Holding Register)等关键的高性能优化技术,使Cache能够同时处理多个未完成的缺失请求。后续章节还将讨论预取(Prefetching)、Cache一致性协议(Coherence Protocol)、目录协议等更高级的主题。

展望2030年代的处理器设计,Cache系统面临着一系列新的挑战和机遇:

  • 容量的持续增长。随着处理器核心数从当前的8\sim16个增长到32\sim64个甚至更多,LLC的容量需求将持续增长(可能达到256 MB甚至512 MB)。3D堆叠SRAM技术(如AMD的3D V-Cache,已在Zen 3和Zen 4中实现了96 MB和128 MB的L3 Cache)为大容量Cache提供了新的实现途径,但也带来了散热和接口设计的挑战。在2 nm和1.4 nm工艺下,SRAM位单元的面积缩减速度已经显著放缓(从每代约30%降低到每代约10%\sim15%),传统的二维SRAM面积扩展日益困难,3D堆叠可能成为增加Cache容量的主要手段。

  • 新型工作负载的挑战。面向AI和机器学习的工作负载(如大型语言模型推理、矩阵乘法、卷积神经网络)具有与传统CPU工作负载截然不同的访存特征:高带宽需求、规则的访问模式、大型工作集。传统的Cache层次结构可能需要针对这些新型工作负载进行根本性的重新设计,例如引入可重配置的Cache/Scratchpad混合结构。在这种混合结构中,SRAM阵列可以动态地在Cache模式(硬件自动管理,利用地址映射和替换策略)和Scratchpad模式(软件显式管理,提供确定性的延迟和带宽)之间切换,以适应不同工作负载的特征。

  • 先进替换策略的落地。基于机器学习的Cache替换策略(如Hawkeye、Glider等)已在学术研究中展现出接近OPT的替换质量,但其训练和推理的硬件开销仍然较大。如何以可接受的硬件开销(几百字节的存储和简单的组合逻辑)将这些先进策略落地到实际处理器中,是2030年代Cache设计者面临的重要课题。一种有前景的方向是使用离线训练、在线推理的模式:在设计阶段使用大量工作负载训练一个紧凑的决策模型(如小型决策树或查找表),将训练好的模型参数烧入处理器的配置寄存器或ROM中,运行时只进行简单的表查找和比较操作。

  • 安全性考量。近年来出现的Cache侧信道攻击(如Spectre、Meltdown、Prime+Probe、Flush+Reload等)利用Cache的状态变化来泄漏敏感信息。这些攻击的核心原理是:攻击者可以通过精心构造的访问序列来推断出受害者程序的Cache访问模式,从而推断出敏感数据(如密码学密钥)。

    2030年代的Cache设计需要在不显著牺牲性能的前提下抵御这些攻击,可能的防御手段包括:

    • Cache分区(Cache Partitioning):为不同的安全域(如不同的进程、虚拟机或安全等级)分配独立的Cache Way或Cache Set,防止跨域的Cache干扰。Intel的Cache Allocation Technology(CAT)允许软件通过Model-Specific Register(MSR)为不同的Class of Service分配LLC的Way子集。

    • 随机化映射(Randomized Mapping):使用密钥相关的哈希函数来计算Cache的Index,使得攻击者无法预测特定地址映射到哪个Cache Set。CEASER和ScatterCache是两种代表性的随机化Cache方案。

    • 不可预测的替换:在替换策略中引入随机性,使攻击者无法通过观察替换行为来推断Cache的占用状态。

  • 异构计算中的Cache设计。随着大小核(big.LITTLE/hybrid)架构的普及,同一芯片上的不同核心可能拥有完全不同的Cache配置。例如,Apple M系列处理器的P-core L1D为128 KB/8-way,而E-core L1D仅为64 KB/4-way。如何在异构核心之间高效地共享LLC、如何在核心迁移时最小化Cache冷启动开销、如何在异构环境下维护Cache一致性——这些都是2030年代需要解决的设计问题。

  • Chiplet架构下的Cache互联。随着Chiplet(小芯片)架构的兴起(如AMD的多CCD设计),Cache层次结构不再局限于单个die内部。跨Chiplet的Cache访问需要通过die-to-die互联(如AMD的Infinity Fabric或UCIe标准),延迟远高于die内部的互联。如何在Chiplet架构下设计高效的分布式LLC、如何优化跨die的一致性流量,是2030年代大规模多核处理器设计的核心挑战之一。

    在AMD的Zen 3/Zen 4架构中,每个CCD(Core Complex Die)包含一个8核心共享的32 MB(或3D V-Cache扩展到96 MB)L3 Cache。跨CCD的Cache访问延迟约为die内L3延迟的2\sim3倍(从\sim40周期增加到\sim100周期),这使得工作负载在CCD之间的分配策略对性能产生重大影响。操作系统的调度器需要感知NUMA(Non-Uniform Memory Access)拓扑,尽量将相关线程调度到同一CCD内部,以最大化L3 Cache的局部性利用。

  • 新型存储器技术的影响。新兴的嵌入式存储器技术(如STT-MRAM、SOT-MRAM)有望在未来取代部分SRAM应用场景。与SRAM相比,MRAM具有更高的密度(位单元面积约为6T SRAM的1/31/21/3\sim1/2)和非易失性(断电不丢失数据),但写延迟较长(\sim10 ns vs. SRAM的\sim1 ns)且写功耗较高。因此,MRAM更适合用于对写延迟不敏感的结构,如LLC、TLB或某些预取器的存储表。2030年代可能出现SRAM和MRAM混合的Cache层次结构——L1/L2使用SRAM保证低延迟,L3/L4使用MRAM提供大容量和低待机功耗。

这些问题使得Cache的设计远比表面上看起来更加复杂,也使得Cache研究在计算机体系结构领域始终保持着旺盛的生命力。

从更宏观的视角来看,Cache的本质是一种以空间换时间的策略——用有限的快速存储器来缓存无限(或极大)慢速存储器中的热点数据。这种策略之所以有效,归根结底取决于程序的局部性特征。一个没有局部性的程序(如纯随机访问一个远大于Cache的地址空间),Cache的命中率将趋近于零,无论Cache多大、相联度多高、替换策略多精巧。幸运的是,绝大多数实际程序都展现出良好的局部性,这使得Cache层次结构成为现代计算机系统中投入产出比最高的硬件优化之一。

理解本章的基础知识是深入学习后续高级Cache技术的前提。读者应当牢记以下几个核心要点:

  1. 局部性是Cache有效的根本原因——时间局部性决定了Cache应保留最近访问的数据,空间局部性决定了Cache应以整行为传输单位。

  2. AMAT公式是评估Cache性能的基本工具——通过分层递归计算平均访存延迟,可以量化每个设计参数(命中率、命中延迟)对整体性能的影响。

  3. SRAM的物理特性直接约束了Cache的容量、延迟和功耗——6T位单元的读稳定性与写裕度矛盾、位线延迟随行数增长、面积随端口数平方增长。

  4. 相联度的增加可以减少冲突缺失但增加硬件复杂度和Way MUX延迟;8-way通常是L1 Cache的最佳平衡点。

  5. VIPT约束C/WC/W \le页面大小是L1 Cache容量和相联度选择的硬性限制,直接影响了处理器的Cache配置。

  6. Multi-banking是现代L1 D-Cache多端口实现的主流方案,通过地址交叉将Data SRAM分Bank,以标准单端口SRAM实现多端口访问。

  7. 写回策略是高性能处理器L1/L2 Cache的标准配置,Write-Back Buffer和Store Buffer是写路径上的关键缓冲结构。

  8. 替换策略的选择取决于Cache层级——L1用Tree-PLRU(低开销、接近LRU),LLC用RRIP/SHiP(抗扫描、感知重用距离)。

  9. Cache访问流水线的级数决定了Load-to-Use延迟——推测性唤醒可以在不增加物理级数的情况下降低有效延迟。

设计提示

前向桥接。本章建立了Cache的基本原理,但一个关键问题尚未展开:如何进一步提高Cache的性能?AMAT公式的三个分量——thitt_{\text{hit}}、缺失率和缺失代价——每一个都有专门的优化技术。第 6.0 章将系统地讨论这些技术:Cache流水线化和路预测降低thitt_{\text{hit}},多级Cache和Victim Cache降低缺失率,预取技术在数据被需要前将其提前取入Cache来降低有效缺失代价。特别值得关注的是路预测(Way Prediction)——它是Cache中引入"投机"的典型例子:通过预测数据在哪一路来避免读取所有路的数据SRAM,以预测错误时1\sim2周期的惩罚换取70%以上的功耗节省。

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