3. 基础 - Cache 体系

296 阅读12分钟

一、Cache 的由来

1. 为什么需要Cache

解决计算机系统中一个核心矛盾:处理器的高速运算能力与相对低速的主存储器(如内存)访问速度之间的巨大差距。简单来说,CPU 太快了,而内存太慢了,如果每次 CPU 需要数据或指令都直接访问内存(DRAM),CPU 大部分时间都会在等待,造成巨大的性能浪费。 缓存作为 CPU 和主存之间的高速缓冲区,存储最近或频繁使用的数据,当 CPU 需要数据时,如果能从比内存快得多的缓存中找到(命中),就能极大地减少等待时间,显著提升系统整体性能。

2. 设计基础:局部性原理

【时间局部性】如果一个数据项被访问,那么在不久的将来它很可能再次被访问,因为程序中存在循环。 【空间局部性】如果一个数据项被访问,那么与它地址相近的数据项很可能很快也会被访问,因为指令通常是顺序存放,顺序执行的。 看下面一个程序示例: 在这里插入图片描述 如果数据在内存中的存放顺序如下:

a[0][0] -> a[0][1] -> a[0][2] -> a[0][3]

对于程序A

sum = a[0][0] + a[0][1] + a[0][2] + a[0][3]

对于程序B]

sum = a[0][0] + a[1][0] + a[2][0] + a[3][0]

可以看出,程序A访问数据顺序和存储顺序一致,空间局部性更好。 Cache(SRAM)就是通过利用程序访问的局部性原理,在高速但容量小的存储介质中保留低速但容量大的主存(如 DRAM)数据副本,从而大幅减少 CPU 访问主存的延迟。

3. 实际系统中的 Cache

在这里插入图片描述 整个系统的存储架构包括了 CPU 的寄存器,L1/L2/L3 CACHE,DRAM 和硬盘。数据访问时先找寄存器,寄存器里没有找 L1 Cache, L1 Cache 里没有找 L2 Cache,依次类推,最后找到主存中。可以看到,速度与存储容量的折衷关系。容量越小,访问速度越快,同时价格也越贵。

二、Cache的工作原理

1. Cache和主存的映射方式

在这里插入图片描述

主存和 Cache 之间以固定大小的块(Cache Line)为单位传输数据(常见 64 字节)。主存被划分为若干块,每个块通过地址映射到 Cache 的特定位置。 Cache 的本质:映射关系 + 数据副本 在这里插入图片描述 Cache 既存储主存数据的副本,也维护这些副本与主存地址的映射关系。Cache和主存的映射方式有三种:

  • 直接映射(Direct mapped):主存中的每一个数据块只能被放置在缓存中唯一一个特定的位置。这种映射关系是通过一个固定的数学计算(通常是取模运算)来确定的。
  • 全相连(Fully associative):主存中的任何一个数据块可以被放置到缓存中的任意一个空闲行(Cache Line)。
  • 组相连(set associative):是直接映射缓存和全相联缓存的折中方案,可以放在Cache的某几行。 映射方式对性能的影响 在这里插入图片描述

2. 数据查找过程

2.1 直接映射(Direct mapped)

主存地址划分:为了找到主存地址 Addr 对应的数据在缓存中的位置(如果存在),需要将 Addr 分解为三个部分:

 标记 (Tag): 地址的最高有效位部分。用于唯一标识一个可能映射到该缓存行的主存块范围。 
 索引 (Index):地址的中间部分。直接决定了这个主存块应该被放置在缓存中的哪个行(行号)。索引字段的位数决定了缓存有多少行(S = 2^索引位数)。
 块内偏移 (Block Offset): 地址的最低有效位部分。用于定位数据块内具体的字节。偏移字段的位数决定了数据块的大小(B = 2^偏移位数)。

直接映射缓存结构划分:缓存被划分为 S 个缓存行。每个缓存行包含:

 有效位 (Valid bit): 标记该行中的数据是否有效(1=有效,0=无效)。 
 标记位 (Tag):用于标识当前存储在该行中的数据块来自主存的哪个区域。标记位的长度取决于主存地址空间的大小和缓存行数。
 数据块 (Data Block):实际存储从主存复制过来的数据。块大小通常是 B 字节(例如 32B, 64B)。

数据查找过程

 a. 提取索引: CPU 给出要访问的内存地址 Addr。硬件首先提取出 Index 字段。
 b. 定位缓存行: 使用 Index 的值作为行号,直接定位到缓存中唯一对应的那一行。
 c. 检查有效位和标记:
 	检查该行的 Valid bit 是否为 1(有效)。
 	比较该行的 Tag 字段与地址 Addr 中的 Tag 字段是否完全一致。
 d. 判断命中/缺失:
 	命中 (Cache Hit): 如果 Valid bit = 1 且 Tag 匹配,则表示所需数据就在这个缓存行中。然后根据 Block Offset 从数据块中取出目标字节/字,返回给 CPU。
 	缺失 (Cache Miss): 如果 Valid bit = 0 或 Tag 不匹配,则表示所需数据不在缓存中(缓存缺失)。

2.2 全相连(Fully associative)

缓存结构,每个缓存行包含:

 有效位 (Valid bit): 标记该行数据是否有效。
 标记位 (Tag): 完整的主存块地址(因为无索引字段)。用于唯一标识该行存储的是哪个主存块。
 数据块 (Data Block): 存储实际数据。
 脏位 (Dirty bit): (用于写回策略)。
 替换信息位 (Replacement Information): (如LRU位)记录行的使用情况。

主存地址划分:

主存地址 Addr 被划分为仅两部分: 
标记 (Tag): 地址的绝大部分(甚至全部)。因为不再需要索引(Index)字段,Tag 直接对应完整的主存块起始地址(除块内偏移外的部分)。
块内偏移 (Block Offset): 地址的最低有效位,定位数据块内的字节。

数据查找过程

a. CPU给出地址 Addr。
b. 并行查找与比较:
    同时读出缓存中所有 M 个行的 Valid bit 和 Tag 字段。
    将 Addr 中的完整 Tag 与所有 M 个行的 Tag 进行并行比较。
    同时检查每个行的 Valid bit。
c. 判断命中/缺失:
    命中 (Cache Hit): 如果在任何一行找到 Valid=1 且 Tag 完全匹配,则命中。根据 Block Offset 取出数据。
    缺失 (Cache Miss): 如果没有任何一行同时满足 Valid=1 和 Tag 匹配,则缺失。

2.3 组相连(set associative)

主存地址划分和直接映射主存地址划分一致。 组相连缓存结构:缓存被组织成一个二维结构:

行 (Rows): 物理上的缓存行。
组 (Sets): 逻辑上的分组,每组包含 N 个行。
每个缓存行包含:
有效位 (Valid bit): 标记该行数据是否有效。
标记位 (Tag): 标识主存块的来源。
数据块 (Data Block): 存储实际数据。
脏位 (Dirty bit): (可选,用于写回策略)标记该行数据是否被修改过。
替换信息位 (Replacement Information): (如LRU位)记录组内各行的使用情况,用于决定替换哪一行。

数据查找过程

a. 提取组索引: CPU给出地址 Addr,硬件提取 Set Index。
b. 定位组: 使用 Set Index 的值找到唯一对应的那个组(Set)。
c. 组内并行查找与比较:
   同时读出该组内 N 个缓存行的 Valid bit 和 Tag 字段。
   将 Addr 中的 Tag 与组内 N 个行的 Tag 进行并行比较。
   同时检查每个行的 Valid bit。
d. 判断命中/缺失:
   命中 (Cache Hit): 如果在组内找到任何一行满足 Valid=1 且 Tag 匹配,则表示命中。然后根据 Block Offset 从该命中行的数据块中取出数据。
   缺失 (Cache Miss): 如果在组内没有任何一行同时满足 Valid=1 和 Tag 匹配,则表示缺失。

3. Cache数据的替换

3.1 替换时机

  • 缓存未命中(Cache Miss):CPU 读写的地址不在缓存中,需要从主存加载。
  • 缓存已满:目标数据映射到的缓存组(Set)所有行(Way)均被有效数据占用(无空闲行)。

3.2 替换策略

  • 最近最少使用(LRU - Least Recently Used):优先替换最久未被访问的缓存行。
  • 伪LRU(Pseudo-LRU):用二叉树等简化结构近似 LRU,牺牲精度换低成本。
  • 先进先出(FIFO - First-In First-Out):替换最早进入缓存的行(按加载时间排序)。
  • 随机替换(Random):随机选择组内一行替换。
  • 最不经常使用(LFU - Least Frequently Used):替换访问次数最少的行(统计历史频率)。 不同层级缓存的策略选择 在这里插入图片描述

补充:现代 CPU(如 Intel/AMD)L3 Cache 普遍使用 类随机策略(如 DRRIP)或 动态策略(根据程序行为调整)。

4. Cache和主存数据的一致性

Cache 是临时缓存。如果发生了写操作,就会造成Cache和主存中的数据不一致,那如何保证写数据操作正确?

4.1 写直达(Write-Through)

CPU 写 Cache 时同步写主存,保持两者实时一致。 在这里插入图片描述

缺点:

  • 每次写操作都访问慢速主存 → 性能瓶颈(如写入频繁时带宽饱和)。
  • 不适用现代高性能 CPU(仅用于特殊场景如 GPU 显存管理)。

4.1 写回(Write-Back) (主流方案)

写操作只更新 Cache,并标记为 脏(Dirty)。脏行仅在替换时或强制冲刷时写回主存。 在这里插入图片描述

减少主存访问次数(合并多次写操作)可以让CPU性能显著提升。

5. 命中率

Cache 命中率(Cache Hit Rate) 是衡量缓存系统效率的核心指标,定义为缓存命中次数占总访问次数的百分比,它直接反映了缓存减少低速存储访问的有效性,计算公式为: 在这里插入图片描述 示例 在这里插入图片描述 测量命中率 测试程序

#include <stdio.h>
#include <stdlib.h>

#define SIZE 10000
#define ITERS 100

int main() {
    int *arr = malloc(SIZE * SIZE * sizeof(int));
    volatile long long sum = 0;

    // 初始化数组
    for (int i = 0; i < SIZE; i++) {
        for (int j = 0; j < SIZE; j++) {
            arr[i*SIZE + j] = i + j;
        }
    }
    
    // 场景一:行优先访问测试
#if 1
    for (int iter = 0; iter < ITERS; iter++) {
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                sum += arr[i*SIZE + j];
            }
        }
    }
#endif

#if 0
    // 场景二:列优先访问测试
    for (int iter = 0; iter < ITERS; iter++) {
        for (int j = 0; j < SIZE; j++) {
            for (int i = 0; i < SIZE; i++) {
                sum += arr[i*SIZE + j];
            }
        }
    }
 #endif

    free(arr);
    return 0;
}

测试方式

// Ubuntu 上需要调整文件控制性能监控的访问权限
sudo sh -c "echo -1 > /proc/sys/kernel/perf_event_paranoid"

perf stat -e cache-misses,cache-references ./program

测试结果

场景一

在这里插入图片描述

场景二

在这里插入图片描述 ubuntu 上测试没有体现出来,但是可以看到行优先访问所花费的时钟周期要远小于列访问的。

典型命中率参考值

在这里插入图片描述

关键洞察:L1 命中率 < 90% 或 L3 命中率 < 70% 通常表明程序有优化空间!

三、Cache一致性

1. Cache一致性问题根源

在这里插入图片描述

2. 硬件解决方案:缓存一致性协议

通过状态机+总线消息维护一致性,定义4种缓存行状态: 在这里插入图片描述

3. 总线通信机制

3.1 总线嗅探(Bus Snooping)

原理:所有核心监听总线消息,发现地址冲突时响应请求。 消息类型: 在这里插入图片描述

3.2 点对点互联(现代替代方案)

问题:总线嗅探在核心数>32时带宽瓶颈严重。 目录协议(Directory Protocol):中央目录记录缓存行状态(如 Intel Mesh, AMD Infinity Fabric)。 优势:仅通知涉及的核心,减少广播流量。

4. 软件协同机制

4.1 内存屏障(Memory Barrier)

解决指令重排导致的一致性问题:

// 核心A:
store X = 1;
store_barrier();  // 确保X写入先于flag
store flag = 1;

// 核心B:
while (load flag != 1);  // 等待flag
load_barrier();         // 确保flag读取后获取最新X
load value = X;         // 此时X必定为1

4.2 缓存维护指令

在这里插入图片描述

5. 总结

硬件基石:MESI/MOESI 协议 + 总线/目录通信 → 自动化解决多数一致性问题。 软件关键:内存屏障 + 缓存维护指令 → 处理极端场景和持久化。 性能核心:避免伪共享 + 减少共享写入 → 降低协议开销。

四、调试方式

1. 查看系统缓存信息

# CPU 缓存拓扑
lscpu | grep cache

# 详细缓存信息
cat /sys/devices/system/cpu/cpu0/cache/index*/size
cat /sys/devices/system/cpu/cpu0/cache/index*/ways_of_associativity

2. perf

// 列出所有可监控的缓存相关事件
perf list | grep -E 'cache|TLB'

// 基本用法
perf stat -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-loads,LLC-load-misses,LLC-loads,dTLB-load-misses,dTLB-loads ./your_program

// 实时监控动态查看缓存事件变化
perf top -e cache-misses,cache-references

// 检测伪共享(False Sharing)
perf c2c record ./your_program
perf c2c report --stdio

// 分析内存访问模式
perf mem record ./your_program
perf mem report --stdio

3. ftrace - 内核函数跟踪

// 启用缓存相关跟踪
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_alloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/mm_page_free/enable

// 捕获数据
cat /sys/kernel/debug/tracing/trace_pipe

4. valgrind + cachegrind

valgrind 的 cachegrind 工具是一个强大的缓存和分支预测模拟器,它通过动态二进制插桩技术模拟程序执行期间的缓存行为。

# 模拟缓存层次结构
valgrind --tool=cachegrind ./your_program

# 生成详细报告
cg_annotate cachegrind.out.<pid>

关键指标:
Ir:指令读取
I1mr:L1指令缓存未命中
D1mr:L1数据缓存未命中
LLmr:末级缓存未命中

基于第二节中命中率测试结果如下: 列访问 在这里插入图片描述 行访问 在这里插入图片描述 可以看到 D1 miss rate 及 L1 Data Cache 在行访问中为2.2%,列访问为32.5%。

五、调试方式选择

在这里插入图片描述