Cache性能

Cache写策略、多级缓存设计与性能量化分析方法

Posted by CloudingYu on April 29, 2026

一、替换策略深入分析:LRU vs MRU

1.1 回顾:几种替换策略

常见替换算法:

  • FIFO:谁先进来谁先走。
  • LRU(Least Recently Used):通过计数器记录最近使用情况,最近最少使用的被替换。
  • MRU(Most Recently Used):替换掉最近刚使用过的块。

直觉上觉得 LRU 应该总是好的,因为数据有时间局部性和空间局部性。但深入到具体数据的时候,发现所有感觉上对的东西不一定对。

1.2 例题参数设定

参数
主存大小 32K 个字(15 位地址)
Cache 容量 4K,四路组相联
每块大小 64 字
主存总块数 $32\text{K} / 64 = 512$ 块($2^9$,块号 9 位)
组数 16 组,每组 4 行
地址划分 字号 6 位 + 组号 4 位 + 标记 5 位
Cache 比主存快 10 倍

访问模式:从地址 0 到 4351 顺序访问(共 4352 个字),即前 68 块,连续循环访问 10 次。

1.3 LRU 策略分析

第一次循环(块 0 ~ 67):Cache 初始为空,所有 68 块全部 miss(冷启动缺失,compulsory miss)。第 0 组的 4 行依次装入块 0、16、32、48。当访问到第 64 块时,第 0 组已满,LRU 选择最久未用的块 0 替换掉。

第二次循环开始:再次访问块 0 时发现已被替换,miss。关键问题:68 块 > 64 行(Cache 容量),每次循环总有 4 块反复被替换。

命中率计算:总访问次数 $4352 \times 10 = 43520$ 字,冷启动 miss 68 次,后续 9 次循环中被替换的块每次第一个字都会 miss。最终命中率约 99.43%,速度提升约 9.5 倍

1.4 MRU 策略分析

第一次循环同样 68 块全部 miss。关键区别在替换决策:当第 64 块进来时,MRU 替换掉刚用过的块 48(而非最久未用的块 0)。第二次循环访问块 0 时,块 0没有被替换,仍在 Cache 中,命中。访问块 1、2、…、47 都命中,到块 48 时 miss。

命中率更高,速度提升约 9.7 倍

1.5 结论

别看只是从 9.5 倍提升到 9.7 倍,在明确的循环数据访问模式下,MRU 可能比 LRU 更优。Memory 命中率的微小提升对代码执行效率影响很大。

这个例子说明:直觉不一定靠谱,必须用具体数据验证。替换策略的选择取决于实际访问模式,没有绝对最优。当工作集略大于 Cache 容量时,LRU 会反复替换即将用到的数据,MRU 反而能保留更多有用数据。


二、Cache 写策略

2.1 问题引出

读操作简单——命中就读,不命中就调入。但写操作引发 Cache 与主存的一致性问题:写了 Cache 之后,主存中的数据就”过时”了。

2.2 Write Through(写直达/写穿)

每次写 Cache 时,同时写主存。优点:Cache 和主存始终保持一致,逻辑简单。缺点:写主存速度慢,CPU 每次写都要等主存完成。

Write Buffer 优化

加一个小的写缓冲队列(通常 4 项):CPU 写数据先写到 Write Buffer,Buffer 告诉 CPU “写好了”,Buffer 在后台慢慢往主存写。如果 Buffer 满了,CPU 必须等待(stall)。怎么解决 Buffer 饱和?加二级 Cache。

2.3 Write Back(写回)

写时只改 Cache,不立即写主存。仅在 Cache 块被替换时,才把修改过的数据写回主存。需要增加 Dirty 位(脏位):

Dirty 含义 替换时操作
0 Cache 与主存一致 直接丢弃,用新数据覆盖
1 Cache 已被修改 必须先写回主存,再加载新数据

2.4 写分配 vs 非写分配

当写操作发生 miss 时:

非写分配(No Write Allocate)

写 miss 时不调入 Cache,直接写主存。缺点:刚写的数据后续访问时 Cache 中没有,需要再从主存读入。

写分配(Write Allocate)

写 miss 时先把块调入 Cache,再在 Cache 中写。优点:利用数据局部性,后续访问该数据大概率命中。

对比例子:对同一地址先写后读 5 次——非写分配:4 次 miss + 1 次 hit。写分配:2 次 miss + 3 次 hit。


三、多核 Cache 一致性问题

3.1 问题描述

多个 CPU 各自有 Cache,共享一个主存。当一个 CPU 写了数据后,另一个 CPU 的 Cache 中还是旧数据。

3.2 两个容易混淆的概念

概念 含义
Consistency(一致性) Cache 与主存之间数据保持一致
Coherence(一贯性/缓存一致性协议) 多个 Cache 副本之间数据保持相同

解决方案需要设计复杂的一致性协议(如 MESI 协议),后续课程会详细讲解。

3.3 GPU vs CPU 的 Cache 差异

  CPU GPU
Cache 设计复杂,追求高命中率 Cache 很小,显存很大
任务特点 控制流复杂,数据依赖多 计算简单且高度并行
数据局部性 需要 Cache 支撑 流式处理,局部性差
多核协作 需要 Cache 一致性协议 主要靠共享显存

四、多级 Cache 设计

4.1 L1 Cache:分离式设计

L1 分为 I-Cache(指令缓存)和 D-Cache(数据缓存)。好处:指令和数据访问不冲突,数据带宽翻倍。L1 的设计目标:命中时间(Hit Time)尽可能短。

4.2 L2 Cache:统一式设计

L2 采用联合(统一)设计,指令和数据合并存放。原因:L2 miss 后要访问主存,代价极大,所以 L2 的命中率必须尽可能高。分开存放会使每块的 size 变小,导致命中率下降。L2 的设计目标:命中率(Hit Rate)尽可能高。

4.3 实际处理器 Cache 参数

Intel Core i7

层级 容量 关联度 说明
L1 32KB 8 路组相联 分离式(I-Cache + D-Cache)
L2 256KB 8 路组相联 每核独享
L3 8MB 16 路组相联 多核共享,负责与主存一致性

谁连主存谁辛苦——L3 是共享的,和主存之间的接口压力最大。

4.4 多级 Cache 性能计算例题

参数:无 Cache 缺失时 CPI = 1,CPU 频率 5GHz,访问主存时间 100ns = 500 个时钟周期,L1 缺失率 2%,L2 访问时间 5ns = 25 个时钟周期,L2 全局缺失率 0.5%。

只有 L1:有效 CPI = 1 + 0.02 × 500 = 11,性能下降约 11 倍。

加入 L2 后:有效 CPI = 1 + 0.02 × 25 + 0.005 × 500 = 4.0,性能损失降到约 4 倍(相比无 L2 时的 11 倍,大幅改善)。

与其花大力气做指令集并行、数据并行,不如把 Cache 做大一点、命中率提高一点,效果更直接。


五、Cache 大小与块大小的选择

5.1 Cache 容量

越大越好,但存在边际递减效应。从 256KB 到 512KB,成本翻倍但命中率提升很小。要根据性价比选择。

5.2 块大小(Block Size)

块大小 问题
太小 无法充分利用空间局部性,miss 率高
太大 Cache 行数减少,替换频繁,miss 率反而上升
最佳 通常 32 ~ 64 字节(根据实验数据曲线确定)

六、存储器带宽优化:三种组织方式

6.1 问题背景

Cache miss 后需要从主存读取一个 block(假设 4 个 word)。不同的主存组织方式对 miss penalty 影响很大。

基本参数:CPU 发地址到内存 1 个周期,内存访问时间 10 个周期,传输一个 word 1 个周期。

6.2 方案一:One Word Wide(单字宽)

每次只传 1 个 word,传 4 个 word 需要 4 次完整的访问-传输。Miss Penalty = 1 + 4 × (10 + 1) = 45 个周期

6.3 方案二:Wide Memory(宽存储器)

把 memory 位宽加大为 4 个 word,一次全部取出。Miss Penalty = 1 + 10 + 1 = 12 个周期。优点:速度快。缺点:需要更宽的总线和更多芯片,花钱多。

6.4 方案三:Interleaved Memory(交叉存储/流水线方式)

利用数据访问的顺序性,在第一个 word 的 memory 准备过程中就开始发第二个 word 的地址,形成流水线式访问。Miss Penalty = 1 + 10 + 3 × 1 + 1 = 15 个周期。优点:代价小(不需要加宽总线),速度快。

方案 Miss Penalty 硬件成本 评价
单字宽 ~45 周期 太慢
宽存储器 ~12 周期 太贵
交叉存储 ~15 周期 又快又省——最优

七、Cache 性能量化分析

7.1 核心公式

平均存储器访问时间(AMAT): \(\text{AMAT} = \text{Hit Time} + \text{Miss Rate} \times \text{Miss Penalty}\)

CPU 时间: \(\text{CPU Time} = IC \times (CPI_{\text{execution}} + \text{Memory Stall Cycles per Instruction}) \times T_{\text{clk}}\)

其中: \(\text{Memory Stall} = \text{每条指令的存储器引用次数} \times \text{Miss Rate} \times \text{Miss Penalty}\)

7.2 CPU 时间计算例题

参数:缺失代价 200 个时钟周期,每条指令正常执行 1 个时钟周期,平均缺失率 2%,每条指令平均 1.5 次存储器引用。

Memory Stall per Instruction = 1.5 × 0.02 × 200 = 6 个周期。有效 CPI = 1 + 6 = 7。存储器停顿导致有效 CPI 从理想的 1 膨胀到了 7,性能下降 7 倍。

算法优化的重点不是把 100 次计算变成 80 次,而是提高访问存储器的命中率。


八、程序优化与 Cache 命中率

8.1 二维数组访问顺序的影响

对于按行优先存储的二维数组 A[i][j]

  • 按行访问(外层 i,内层 j):访问顺序与存储顺序一致,空间局部性好,Cache 命中率高。
  • 按列访问(外层 j,内层 i):每次跳跃整行访问,空间局部性极差,频繁 miss。

8.2 命中率量化计算

参数:32 位机器,256MB 地址空间($2^{28}$ 字节,按字节编址),指令 Cache 和数据 Cache 各 8 行,主存块大小 64B,直接映射。

地址划分:Block Offset 6 位,Index 3 位,Tag 19 位。

按行访问的命中率:共 64K 次访问,占 4K 个主存块。每个主存块的第一个元素 miss(compulsory miss),块内后续 15 个元素命中。命中率 = 1 - 4K/64K = 93.75%

按列访问的命中率:如果数组跨越多个 Cache 行的映射范围,每次访问都可能 miss。极端情况下命中率可降至 0%

代码逻辑完全相同,性能可以差一个数量级。数据布局和访问顺序是 Cache 优化的核心。


能把知识写出来才是真正学会了——会用 < 能给别人讲明白 < 能严谨地写出来。