内存层次:Cache、带宽、局部性与一致性 / Memory Hierarchies, Bandwidth, Locality, and Coherence
📅 创建时间:2026-07-20 🏷️ 标签:#内存层次 #Cache #带宽 #局部性 #一致性 📚 前置知识:[[03-cpu-parallelism]] 📚 相关知识:[[/02-systems-and-performance/02-computer-architecture-and-hardware/01-computer-architecture-basics]] [[/02-systems-and-performance/02-computer-architecture-and-hardware/05-cuda-kernel-and-memory]]
1. 为什么数据移动决定性能
计算单元速度远高于主存。处理器可以在等待一次内存访问期间执行许多算术操作。
典型层次如下:
寄存器 最快、最小、线程私有
L1 Cache 很快、每核或局部共享
L2 Cache 较大、延迟更高
L3 Cache 多核共享、容量更大
主存 DRAM 容量大、延迟高
SSD/网络 更慢,但容量或范围更大从上往下,容量增加,访问速度下降。
2. 时间局部性与空间局部性
时间局部性
最近访问过的数据很可能再次访问。
for (int k = 0; k < 100; ++k)
sum += value; // value 被反复使用空间局部性
访问某个地址后,很可能继续访问附近地址。
for (int i = 0; i < n; ++i)
sum += a[i]; // 连续访问Cache 通常以 Cache Line 为单位搬运,例如 64 字节。连续访问能够充分利用一次搬运的数据。
3. 行优先矩阵的访问顺序
C/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];两段代码计算量相同,但第二段可能产生更多 Cache Miss。
4. 分块提高数据复用
矩阵乘法直接访问大矩阵时,数据可能在再次使用前已被逐出 Cache。分块把问题切成能放入 Cache 的小块:
大矩阵
┌────┬────┬────┐
│块00│块01│块02│
├────┼────┼────┤
│块10│块11│块12│
└────┴────┴────┘GPU 的 Shared Memory 分块和 CPU 的 Cache Blocking,本质都是主动提高局部性。
5. 延迟与带宽
- 延迟:一次访问从发出到返回需要多久
- 带宽:单位时间最多能搬运多少数据
链表随机访问常受延迟限制,向量连续扫描更容易接近带宽上限。
算术强度
算术强度 = 浮点运算次数 / 内存传输字节数向量加法读取两个数组并写入一个数组,每个元素只做一次加法,算术强度很低。矩阵乘法可以重复使用数据,算术强度较高。
6. Cache 一致性
多个 CPU 核心可能缓存同一个内存地址。如果一个核心修改数据,其他核心的旧副本必须失效或更新。
一致性协议保证最终看到正确数据,但会产生:
- Cache Line 在核心间转移
- 共享写入导致一致性流量
- 原子变量成为热点
- 伪共享导致无意义失效
因此“共享内存编程方便”不代表共享数据是免费的。
7. 内存一致性模型
编译器和 CPU 为提高性能可能重排指令。多线程程序不能只根据源代码顺序推断其他线程观察到的顺序。
C++ 使用原子操作和内存序建立线程间关系:
data = 42;
ready.store(true, std::memory_order_release);
if (ready.load(std::memory_order_acquire)) {
use(data); // acquire/release 建立可见性关系
}除非确实在实现底层并发结构,否则优先使用 Mutex、并发容器和成熟库,避免自行组合复杂内存序。
8. 内存优化顺序
- 减少不必要的数据和拷贝。
- 让访问尽量连续。
- 选择紧凑的数据布局。
- 通过分块增加复用。
- 减少跨线程共享写入。
- 检查 NUMA 和设备间传输。
- 使用硬件计数器验证 Cache Miss 和带宽。
8. Working Set
Working Set 是阶段内频繁访问的数据集合。
它与进程总内存不同。
输入扩大后,Working Set 跨越 Cache 层次会出现性能拐点。
9. Cache Line
Cache 按 Line 搬运和一致性管理。
只访问一个字段也可能搬运整条 Line。
连续访问提高空间利用。 随机访问可能浪费大部分字节。
10. Cache Miss
常见分类:
- Compulsory;
- Capacity;
- Conflict;
- Coherence。
分类帮助提出假设,但要结合硬件事件与访问模式。
11. Write Allocate
普通 Store 可能先获取 Cache Line 所有权。
覆盖大数组时会产生额外读流量。
Streaming Store 适合特定连续写场景,需要实测。
12. Prefetch
硬件预取器擅长连续和固定步长。
依赖指针链和随机访问难以提前发现。
软件预取距离过近无效,过远会污染 Cache。
13. Memory-Level Parallelism
处理器可同时维护多个未完成请求。
独立访问能隐藏部分内存延迟。
单条依赖链即使带宽未满,也可能很慢。
14. AoS 与 SoA
AoS 让完整对象相邻。 SoA 让同字段连续。
选择依据全部消费者的字段访问和 SIMD 需求。
热冷字段拆分可以减少工作集。
15. TLB
TLB 缓存虚拟到物理页转换。
大工作集和随机访问会增加 TLB Miss。
Huge Page 减少转换,但增加碎片与部署复杂度。
16. Page Fault
区分:
- 首次匿名页;
- 文件映射;
- Minor Fault;
- Major Fault;
- Copy-on-Write;
- Swap。
预触碰只是移动成本发生时间。
17. NUMA
多插槽系统具有本地与远端内存。
CPU node 0 -> memory node 0
CPU node 1 -> memory node 1首次触碰、线程亲和性和分片影响位置。
18. NUMA 策略
- 并行初始化;
- 按节点分片;
- 每节点内存池;
- 只读复制;
- 减少跨节点写;
- 粗粒度归约;
- 测量远端流量。
19. False Sharing
不同线程写同一 Cache Line 的不同字段,也会产生一致性抖动。
线程局部累加、分片和对齐是候选方案。
平台 Cache Line 大小和收益需要测量。
20. 分配器
大量小分配带来元数据、锁、碎片和 TLB 压力。
候选方案:
- 批量;
- Arena;
- Pool;
- Buffer 复用;
- 阶段统一释放;
- PMR。
优化必须保持构造、析构、对齐和线程安全。
21. GPU 内存层次
GPU 包含:
- Registers;
- Shared Memory;
- L1/L2;
- Global Memory;
- Constant/Texture;
- Host Pinned;
- Unified Memory。
每层容量、作用域、带宽和同步不同。
22. 合并访问
相邻线程访问相邻地址更容易合并为少量事务。
Stride 和随机 Scatter/Gather 增加事务。
数据布局应和线程索引一起设计。
23. Shared Memory
Shared Memory 用于 Block 内复用和协作。
需要控制:
- 容量;
- Bank Conflict;
- 同步;
- Tile;
- Occupancy。
24. Unified Memory
统一地址不消除迁移。
观察 Page Fault、Migration、Prefetch 和 Oversubscription。
25. 测量
结合:
- 端到端时间;
- 字节估算;
- Cache/TLB 事件;
- DRAM 带宽;
- NUMA;
- Page Fault;
- 分配 Profile;
- GPU Timeline。
26. 实验
- 工作集扫描;
- Stride;
- AoS/SoA;
- Pointer Chasing;
- False Sharing;
- NUMA First Touch;
- Huge Page;
- GPU 合并访问;
- Shared Memory;
- Unified Memory 迁移。
27. 完成标准
应能解释:
- 工作集;
- 字节流量;
- 延迟与带宽;
- Cache/TLB;
- NUMA;
- False Sharing;
- 分配;
- GPU 内存;
- 测量证据。
核心总结
- 现代并行计算经常受数据移动而不是计算能力限制。
- 局部性决定 Cache 的有效程度。
- 分块是 CPU、GPU 和科学计算中的通用优化思想。
- 多核共享数据会触发一致性成本。
- 必须同时分析延迟、带宽和算术强度。
下一篇:[[05-gpu-architecture]]