内存架构基础:CPU缓存层级(L1/L2/L3)、缓存行与伪共享、TLB与页表

聊内存优化,绕不开CPU缓存。很多同学写代码时觉得内存就是内存,CPU直接读写就完事了。其实不是这样。CPU和内存之间,隔着好几层缓存,每一层都有它的脾气。

我刚开始做性能调优那会儿,就吃过不懂缓存的亏。一个多线程程序,怎么优化都跑不快,最后发现是伪共享在作祟。从那以后,我养成了一个习惯——写性能敏感代码前,先画一张缓存层级图。

CPU缓存层级:为什么需要L1/L2/L3?

先看一张图,帮你快速建立整体认知。

CPU缓存层级架构图 CPU缓存层级架构 CPU Core 0 L1d / L1i L1d / L1i L2 Cache (私有) CPU Core 1 L1d / L1i L1d / L1i L2 Cache (私有) L3 Cache (共享) 主存 (DRAM) L1/L2 每个核心私有,L3 所有核心共享。越靠近核心,容量越小、速度越快。

CPU缓存的核心逻辑很简单:离核心越近,访问越快,但容量越小。具体来说:

  • L1缓存:分指令缓存(L1i)和数据缓存(L1d),典型大小32KB-64KB。访问延迟约3-5个CPU周期。每个核心独享。
  • L2缓存:统一缓存指令和数据,典型大小256KB-512KB。访问延迟约10-15个周期。每个核心独享。
  • L3缓存:所有核心共享,典型大小8MB-32MB。访问延迟约30-50个周期。
我的经验: 写性能测试时,如果数据量超过L3缓存大小,性能会断崖式下跌。我建议你提前算好热点数据的总大小,尽量让它塞进L3甚至L2里。

缓存行与伪共享:多线程性能的隐形杀手

CPU从内存读数据,不是按字节读的,而是按缓存行(Cache Line)为单位。现代x86 CPU的缓存行大小通常是64字节。也就是说,你只想要一个int,CPU却把周围64字节一股脑全搬进缓存。

这本身是好事——利用了空间局部性。但多线程场景下,它可能变成噩梦。

什么是伪共享?

假设两个线程分别操作两个不同的变量,但这两个变量恰好在同一个缓存行里。线程A修改变量a,导致整个缓存行失效;线程B想读变量b,发现缓存行失效了,只能重新从内存加载。明明操作的是不同数据,却因为共享了同一个缓存行而互相拖累。

这就是伪共享(False Sharing)。

核心问题: 缓存一致性协议(如MESI)以缓存行为单位维护状态。只要缓存行内任何一个字节被修改,整个缓存行在其他核心的副本都会失效。

我曾经优化过一个日志系统,多线程写日志时吞吐量上不去。用perf一看,L1缓存miss率异常高。排查后发现,日志缓冲区里的几个计数器挨在一起,正好落在同一个缓存行里。每个线程写自己的计数器,结果互相踢缓存行。嗯,典型的伪共享。

解决方案?给每个计数器前后填充字节,让它们独占一个缓存行。

// 伪共享示例:两个变量在同一个缓存行
struct Bad {
    int a;  // 线程1操作
    int b;  // 线程2操作
};  // a和b很可能在同一个缓存行内

// 修复:缓存行填充
struct Good {
    int a;
    char padding[60];  // 填充到64字节
    int b;
    char padding2[60];
};
注意: 缓存行填充会浪费内存。只在性能关键路径上使用,别到处乱填。我见过有人把每个小结构体都填上64字节,内存占用直接翻了好几倍。

TLB与页表:虚拟地址到物理地址的桥梁

现代操作系统使用虚拟内存。每个进程有自己的虚拟地址空间,CPU需要把虚拟地址翻译成物理地址。这个翻译工作依赖页表(Page Table)。

页表本身存在内存里。每次内存访问都要查一次页表,那性能就完蛋了——等于每次访问都多了一次内存访问。所以CPU引入了TLB(Translation Lookaside Buffer),也就是页表的缓存。

层级 典型大小 覆盖范围 命中延迟
L1 TLB 64-128 条目 256KB-512KB (4KB页) 1个周期
L2 TLB 512-1024 条目 2MB-4MB (4KB页) 5-10个周期
页表遍历 — — 100+个周期

TLB miss的代价非常高。一次TLB miss意味着CPU要走页表遍历流程,可能需要访问多级页表,耗时上百个周期。

大页(Huge Pages)的作用

标准页大小是4KB。如果程序访问了2MB的连续内存,需要512个页表条目。TLB根本装不下。但如果用2MB的大页,只需要1个条目。TLB的覆盖范围瞬间扩大512倍。

我优化过一个内存数据库,数据量几十GB,随机访问模式。启用2MB大页后,TLB miss率从15%降到了不到1%,查询延迟直接降了30%。

实用建议: 如果你的程序有大量随机内存访问,或者工作集超过几十MB,强烈建议启用大页。Linux下通过 echo always > /sys/kernel/mm/transparent_hugepage/enabled 开启透明大页,或者用 mmap 配合 MAP_HUGETLB 手动分配。

一张图总结本章核心

内存架构核心概念关系图 内存架构核心概念关系 CPU缓存层级 缓存行与伪共享 TLB与页表 L1 < L2 < L3 速度递减,容量递增 核心私有 vs 共享 缓存行大小64字节 伪共享 = 缓存行争抢 填充对齐可解决 TLB缓存页表条目 TLB miss代价高 大页提升TLB覆盖

这三个概念是内存性能优化的基石。你想想看,不懂缓存层级,你怎么知道数据该放多大?不懂缓存行,你怎么排查多线程性能问题?不懂TLB,你怎么理解大页的价值?

我个人习惯,拿到一个新系统,第一件事就是看它的内存访问模式。连续访问还是随机访问?工作集多大?线程间有没有共享数据?这些信息直接决定了该用哪种优化手段。