一个反直觉的事实
在内存中访问数据,不同位置的访问速度并不是一样的——因为中间隔着 CPU 缓存。
为什么需要缓存
CPU 的运算速度远快于内存访问速度。一次内存访问可能需要数百个 CPU 周期,而 CPU 执行一条简单指令只需一个周期左右。
如果不加缓存,CPU 大部分时间都在等数据。缓存就是为了填补这个速度鸿沟。
三级缓存结构
现代 CPU 通常有三级缓存:
- L1:最小最快(几十 KB),每个核心独享,访问约 1–4 周期;
- L2:中等(几百 KB 到几 MB),每核独享或共享,约 10 周期量级;
- L3:最大(几 MB 到几十 MB),所有核心共享,约数十周期。
再往下是主内存(DRAM),访问延迟约数百周期。
局部性原理
缓存之所以有效,是因为程序访问存在两种局部性:
时间局部性:刚被访问的数据,很可能很快再次被访问;
空间局部性:刚被访问的数据,其附近的数据很可能很快被访问。
第二种局部性,正是"顺序遍历数组更快"的原因。
缓存行(Cache Line)
缓存不是以字节为单位加载数据的,而是以缓存行为单位,通常 64 字节。
当你访问数组的一个元素时,CPU 会把包含它的整个缓存行(64 字节,可能包含相邻的数个数组元素)一起加载进缓存。
于是,顺序遍历时:访问第一个元素会加载一整行,接下来几个元素的访问就全部命中缓存,无需访问内存。而跳跃访问时,每次都可能加载一个新缓存行,且加载进来的相邻数据还用不上——缓存利用率低。
一个具体例子
遍历一个 int[1000][1000] 二维数组:
- 按行遍历(外层行、内层列):连续访问内存,缓存友好;
- 按列遍历:每次跳跃 1000 个元素的距离,几乎每次访问都是缓存未命中。
在大规模数据上,两者的耗时可能相差数倍,而它们执行的指令数量其实是相同的。
其它缓存相关的问题
伪共享(False Sharing):两个线程分别修改两个不同的变量,但这两个变量恰好落在同一个缓存行里。由于缓存一致性协议以缓存行为单位维护,两个核心会反复使对方的缓存行失效,导致性能急剧下降。解决办法是让变量按缓存行对齐(padding)。
缓存一致性:多核 CPU 中,各核心的 L1/L2 缓存需要保持一致。MESI 等协议通过在缓存行上标记状态(Modified/Exclusive/Shared/Invalid)来协调。
实践意义
理解缓存,是理解很多性能优化手段的前提:数据布局(数组 vs 链表)、结构体字段顺序、循环嵌套顺序、批处理大小的选择,背后都有缓存的影子。
说明:本文为计算机体系结构的公开知识整理。