C 语言缓存与程序性能
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。写 C 的人最容易忽视一个事实:同样的算法,遍历顺序不同,性能可能差 100 倍。这不是玄学,是 CPU 缓存的物理规律。本文用实测数据把"缓存友好"这件事讲透——这是 C 性能优化里回报率最高的一课。
本文要回答的问题
- 为什么"行优先"和"列优先"遍历二维数组,性能差 100 倍?
- CPU 缓存到底怎么工作?什么是缓存行、局部性?
- 怎么写"缓存友好"的 C 代码?
一、实测:遍历顺序差 100 倍
对一个 8192 × 8192 的二维数组求和,两种遍历顺序:
c
// 行优先(顺序访问)
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
sum += a[i][j];
// 列优先(跳跃访问)
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
sum += a[i][j];实测结果(gcc -O2,N=8192):
text
行优先遍历: 40 ms
列优先遍历: 4150 ms同样的数据、同样的计算量、同样的结果(sum 都是 3322022676),列优先慢了整整 100 倍! 唯一的区别就是访问内存的顺序。
二、为什么?缓存行的物理真相
2.1 内存金字塔
CPU 太快,内存太慢——中间靠多级缓存缓冲:
| 层级 | 延迟 | 大小 |
|---|---|---|
| L1 缓存 | ~1 ns(4 周期) | 32-64 KB |
| L2 缓存 | ~4 ns | 256 KB - 1 MB |
| L3 缓存 | ~12 ns | 8-32 MB |
| 主内存 | ~100 ns | 几十 GB |
访问主内存比访问 L1 慢 100 倍。所以"命中缓存"还是"去主内存取",决定了程序快慢。
2.2 缓存行(cache line)
CPU 不按字节、按 64 字节的"缓存行" 为单位加载数据。访问 a[i][j] 时,会把它所在的整个 64 字节缓存行都拉进 L1。
2.3 行优先 vs 列优先的本质
C 的二维数组是行优先存储的——a[i][j] 和 a[i][j+1] 在内存里相邻:
text
内存布局(行优先):
a[0][0] a[0][1] ... a[0][8191] a[1][0] a[1][1] ...
← 同一行连续存放 →- 行优先遍历:依次访问连续内存,每次加载的缓存行里的数据都被用上 → 缓存命中率接近 100% → 40ms。
- 列优先遍历:
a[0][j]到a[1][j]相隔 8192×4=32KB,每次跳跃都跨出缓存行 → 几乎每次都是缓存 miss → 4150ms。

三、缓存友好的 C 代码模式
| 模式 | 缓存友好的做法 | 反例 |
|---|---|---|
| 多维数组遍历 | 按内存布局顺序(行优先) | 列优先 |
| 结构体数组 | 把高频字段放一起、紧凑排列 | 字段散乱、结构体过大 |
| 链表 vs 数组 | 顺序遍历用数组(连续内存) | 链表(节点分散,缓存 miss) |
| 对象大小 | 热点对象尽量 ≤ 缓存行 | 超大的 struct |
核心原则:让"接下来要访问的数据"离"刚访问的数据"在内存上越近越好——这就是空间局部性。
四、与 perf 的配合
缓存问题是"看不见的慢"——CPU 利用率不高,但程序就是慢。用 perf stat 看缓存 miss:
bash
perf stat -e cache-misses,cache-references ./cache-perf列优先版本会看到 cache-misses 飙升。完整用法见 perf 剖析 C 程序。
五、与本站主线衔接
- L3 缓存:缓存层级、缓存行、一致性协议的完整原理,见 L3 内存子系统。
- TLB:大数组访问的页表局部性,见 L3 TLB。
- 结构体对齐:字段布局影响缓存命中,见 结构体布局与对齐深入。
- 伪共享:多线程下缓存行的隐形杀手,见 demos/false-sharing。
一句话总结
缓存是 C 性能的分水岭——行优先 40ms、列优先 4150ms 的 100 倍差距,根因是缓存行按 64 字节加载、顺序访问才命中;写"缓存友好"的代码(按内存布局顺序访问、结构体紧凑、热点数据连续),往往比手写汇编还能提速。