内存层次: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 和带宽。
历史内存层次补充
本页保留课程合并前的内存层次说明。
当前文章扩展了 Working Set、TLB、NUMA、GPU 内存和实验方法。
Working Set
阶段内频繁访问的数据决定实际 Cache 压力。
总文件或总堆大小不能替代 Working Set。
Cache Line
Cache 以 Line 为搬运单位。
字段布局决定有效字节比例。
Miss 分类
- 首次;
- 容量;
- 冲突;
- 一致性。
分类需要结合访问和计数器。
延迟
依赖访问受单次延迟限制。
Pointer Chasing 难以提高并行请求数。
带宽
流式访问更接近带宽限制。
线程增加到带宽饱和后不会继续线性提升。
预取
连续模式适合硬件预取。
随机和数据依赖访问难以预取。
AoS/SoA
AoS 适合完整对象访问。 SoA 适合同字段批量与 SIMD。
布局必须看全部阶段。
TLB
TLB 缓存页转换。
Huge Page 有转换收益与内存管理代价。
Page Fault
首次映射、文件页、COW 和 Swap 代价不同。
区分 Minor 与 Major Fault。
NUMA
本地与远端内存路径不同。
首次触碰和亲和性影响放置。
False Sharing
不同变量位于同一 Line 也会争用。
分片和对齐需要 Profile 证据。
分配器
小对象产生碎片、锁和元数据。
Arena 与 Pool 适合明确生命周期。
GPU Register
Register 快但有限。
过多寄存器可能降低驻留线程。
GPU Shared Memory
Block 内共享,适合 Tile 和复用。
需要同步和避免 Bank Conflict。
GPU Global Memory
容量大,访问应合并并提高复用。
Unified Memory
地址统一,物理页面仍会迁移。
Timeline 用于观察 Fault 和 Migration。
Host Pinned Memory
支持高效异步传输。
过量会影响系统,必须预算。
测量工具
- VTune Memory Access;
- WPA Memory/Heap;
- Linux Perf;
- 带宽计数器;
- NUMA 工具;
- Nsight Systems;
- Nsight Compute;
- 分配 Profile。
实验记录
保存:
- 输入;
- 工作集;
- 布局;
- 线程;
- 亲和性;
- 页面策略;
- 字节估算;
- 实际带宽;
- Cache/TLB;
- 正确性。
历史参数边界
Cache 容量、Line、TLB 和带宽随平台变化。
旧阈值不能直接用于当前硬件。
当前课程映射
当前文章增加:
- Working Set;
- Write Allocate;
- MLP;
- TLB/Page Fault;
- NUMA 策略;
- GPU 内存;
- 实验和完成标准。
阅读完成标准
应能:
- 估算 Working Set;
- 区分延迟和带宽;
- 解释 Cache/TLB;
- 识别 False Sharing;
- 分析 NUMA;
- 选择数据布局;
- 解释 GPU 内存;
- 设计测量。
归档复现清单
- 来源提交;
- CPU/GPU;
- 内存拓扑;
- 编译器;
- 数据布局;
- 输入规模;
- 线程与亲和性;
- 页面大小;
- 预热状态;
- Cache/TLB 事件;
- 带宽;
- Page Fault;
- NUMA 远端流量;
- 原始样本;
- 正确性;
- 当前平台差异。
历史结果无法复现时,应保留原环境说明而不是重新解释为当前保证。
核心总结
- 现代并行计算经常受数据移动而不是计算能力限制。
- 局部性决定 Cache 的有效程度。
- 分块是 CPU、GPU 和科学计算中的通用优化思想。
- 多核共享数据会触发一致性成本。
- 必须同时分析延迟、带宽和算术强度。
下一篇:[[05-gpu-architecture]]