缓存层级:L1/L2/L3缓存结构,缓存行与局部性原理

大家好,我是你们的老朋友。今天我们来聊聊缓存层级这个事。

说实话,很多做算子融合的同学,一开始都栽在缓存上。我自己刚入行那会儿,写了个自以为很牛的矩阵乘法,结果跑起来比单线程还慢。后来一查,原来是缓存没用好。嗯,今天就把这些坑给大家捋清楚。

一、L1/L2/L3 缓存结构

现代CPU的缓存,说白了就是三级金字塔结构。我习惯把它想象成一个「快递分拣中心」:

  • L1 缓存:离核心最近,速度最快(1-2个时钟周期),但容量最小(32KB-64KB)。每个核心独享。
  • L2 缓存:速度稍慢(10-20个时钟周期),容量256KB-1MB。也是每个核心独享。
  • L3 缓存:所有核心共享,速度最慢(30-50个时钟周期),容量8MB-32MB。

你想想看,为什么要有这么多层?

因为「快」和「大」是矛盾的。SRAM做L1可以很快,但贵得要命。DRAM做内存便宜,但慢得让人抓狂。所以折中方案就是:把最常用的数据放在L1,次常用的放L2,再次的放L3,剩下的去内存里找。

核心要点:算子融合的目标,就是让数据尽可能留在L1/L2里,别老往内存跑。

二、缓存行——你根本躲不开的64字节

这里有个概念我必须强调:缓存行(Cache Line)。

CPU从内存读数据,不是按字节读的,而是按「行」读。一行通常是64字节。什么意思呢?

比如你只想读一个int(4字节),CPU会连它旁边的60个字节一起搬进缓存。这就是所谓的「空间局部性」——你读了一个,很可能马上要读旁边的。

我在项目中遇到过一个问题:两个线程各自修改相邻的变量,结果性能暴跌。为什么?因为这两个变量在同一个缓存行里。线程A改了变量,导致整个缓存行失效,线程B不得不重新加载。这就是著名的伪共享(False Sharing)。

避坑指南:我曾经在一个多线程算子里,把两个热点变量放在同一个结构体里,结果性能只有预期的1/3。后来用__attribute__((aligned(64)))把它们分开,速度直接翻倍。

三、局部性原理——算子融合的理论根基

局部性分两种:

  1. 时间局部性:刚用过的数据,很快还会再用。
  2. 空间局部性:刚用过的数据旁边的数据,很快也会用到。

算子融合为什么能提升缓存命中率?说白了就是利用局部性。

举个例子,两个独立的算子:

// 算子A:对数组每个元素加1
for (i = 0; i < N; i++) {
    a[i] = a[i] + 1;
}

// 算子B:对数组每个元素乘2
for (i = 0; i < N; i++) {
    a[i] = a[i] * 2;
}

分开执行时,第一个循环把整个数组从内存搬到缓存,处理完又写回内存。第二个循环再重复一遍。两次全量搬运,缓存利用率极低。

融合之后:

// 融合算子:一次循环搞定
for (i = 0; i < N; i++) {
    a[i] = (a[i] + 1) * 2;
}

每个元素只从内存加载一次,在缓存里完成两次操作,然后写回。缓存命中率从50%左右提升到接近100%。

我的经验:实际项目中,融合后的算子往往能减少30%-50%的内存访问量。但要注意,融合不能无脑做——如果两个算子访问的数据集完全不同,融合反而会污染缓存。

四、一张图看懂缓存层级与局部性

下面这张SVG图,是我自己画的一个缓存层级与算子融合的关系图。你可以看到数据是怎么从内存一层层往上走的:

缓存层级与算子融合数据流 L1 缓存 32KB-64KB | 1-2周期 L2 缓存 256KB-1MB | 10-20周期 L3 缓存 8MB-32MB | 30-50周期 主内存 (DRAM) 算子融合如何利用缓存? ❌ 未融合: 算子A:加载整个数组 → 处理 → 写回 算子B:再次加载整个数组 → 处理 → 写回 缓存命中率:约50% ✅ 融合后: 加载一个缓存行(64字节) → 在L1中完成A和B两个操作 → 写回内存 缓存命中率:接近100% 局部性原理: 时间局部性:同一数据短时间内反复使用 空间局部性:相邻数据被连续访问

五、实战中的缓存优化策略

基于上面的原理,我总结了几条实战经验:

策略 说明 效果
循环融合 将多个遍历同一数据集的循环合并 减少内存访问次数,提升时间局部性
数据对齐 确保热点数据按64字节对齐 避免跨缓存行访问,减少加载次数
分块处理 将大数组切分成适合L1缓存的小块 让数据在L1中完成全部操作,避免频繁换入换出
避免伪共享 多线程中,将不同线程的数据分散到不同缓存行 防止缓存行无效导致的性能抖动

一句话总结:算子融合的本质,就是通过合并计算步骤,让数据在缓存里「一次加载,多次使用」。你想想看,这比来回倒腾内存不知道高到哪里去了。

好了,今天的内容就到这里。记住这些缓存层级和局部性的原理,后面讲具体融合策略时,你会理解得更深。

可可精品资料库