计算机体系结构 22:数据布局与实测解释
计算机体系结构 22:数据布局与实测解释
核心问题
第 16 到第 19 篇讨论了缓存映射、写策略、DRAM 和并发缺失。第 20、21 篇把虚拟地址翻译补上。到这里,优化问题可以回到程序员最常见的选择:同一份逻辑数据,应该按什么布局放进内存。
本文只回答一个可验收的问题:当模型说 SoA 比 AoS 少读 cache line,或者矩阵分块缩小工作集时,怎样把这个模型结论和本机 wall-clock 计时分开解释。
结论先列出来:
- AoS/SoA 的 cache-line 模型显示:只读一个字段时,SoA 的模型缺失数更少。
- 本机计时中,SoA 的中位时间低于 AoS。
- 本机小参数矩阵乘法中,blocked 版本慢于 plain 版本。
- 没有性能计数器,所以本文不声称任何真实 L1 miss 数、带宽或能耗。
优化文章要分成三栏:模型预测、程序产物、机器计时。三栏可以互相解释,但不能互相冒充。
边界
本文的一手依据有两类。CS61C cache blocking 材料支持“分块可以改善数据复用,但必须基于声明的缓存模型”。LLVM auto-vectorization 文档说明 LLVM 有 loop vectorizer 和 SLP vectorizer,并提供 -Rpass=loop-vectorize、-Rpass-missed=loop-vectorize、-Rpass-analysis=loop-vectorize 等诊断选项;这意味着同一段 C 代码的时间变化可能同时来自缓存、向量化、展开、调度和别名分析,不应只归因于 cache。
本文的附件:
- cache_model.py
- cache_model.json
- layout_bench.c
- layout_timings.txt
- layout_timing_summary.json
- layout_cli_validation.txt
- layout_bench.s.txt
- layout_binary_inspection.txt
证据等级:手算、功能执行、真机测量。
真机测量只代表当前 macOS AArch64 环境中这次编译和运行的 wall-clock 样本。执行日期为 2026-09-22,编译器为 Apple clang 21.0.0(clang-2100.3.34.2),每个 case 记录 5 个样本。程序在每个 rep 内固定先跑 AoS 再跑 SoA、先跑 plain 再跑 blocked,因此可能存在顺序、缓存预热、频率和后台负载偏差。没有 perf 计数器、没有硬件 cache miss 记录、没有带宽计数、没有能耗记录。/private/tmp 下新编译 Mach-O 在本环境会退出 137,复现实验使用仓库内 examples/computer-architecture/layout/build/ 路径。
AoS 与 SoA 的模型差异
AoS 把同一个对象的字段放在一起:
1 | |
如果只累加 x 字段,每个 Point 占 16 字节,64 字节 cache line 里放 4 个点。读取 4096 个点的 x 字段,会触碰:
1 | |
SoA 把所有 x 连续放在一个 float x[] 中。64 字节 cache line 里有 16 个 float,读取 4096 个元素触碰:
1 | |
模型输出:
1 | |
这个“misses”来自一个全相联 LRU 教学模型,不是硬件计数器。它表达的是空间局部性差异:只读一个字段时,AoS 会把 y/z/w 一起带进来但不用;SoA 把要读的 x 密集排列。
布局选择取决于访问投影。只读一个字段时 SoA 常有优势;如果每次都要同时读写同一个对象的多个字段,AoS 的相邻字段也可能正好被利用。
本机计时怎样读
复现实验命令:
1 | |
输入解析已做边界检查:负数、超大整数、非法后缀、block > matmul_n、参数过多都会返回错误。验证记录在 layout_cli_validation.txt。
本次 AoS/SoA 计时样本的中位数:
| case | median ns |
|---|---|
| AoS | 715375 |
| SoA | 593167 |
SoA 在这次样本中更快,比例约为:
1 | |
这个结果和模型方向一致,但不能写成“真实 L1 miss 少了 4 倍导致时间快 1.2 倍”。原因至少有三层:
- 编译器可能向量化、展开或改变循环形态。
- 本机 cache 层级、预取器、乱序执行会掩盖一部分 cache-line 差异。
- wall-clock 受后台负载和频率策略影响。
能成立的表述是:在本文参数和编译条件下,SoA 的模型触线数更少,真实 wall-clock 中位数也更低;缺少硬件计数器时,只能说方向一致但不能确定因果,不能升级为实测 cache miss 结论。
分块不是保证加速
矩阵乘法的 plain 版本按 i-k-j 顺序遍历。blocked 版本把 i/k/j 划成 32×32 小块。工作集手算显示:
1 | |
这个数说明 32×32 分块把当前参与计算的 A/B/C 小块控制在 12 KiB 量级。它只说明工作集规模,不说明程序必然更快。
本次计时中位数:
| case | median ns |
|---|---|
| plain matmul | 477333 |
| blocked matmul | 801917 |
blocked 反而更慢。原因不需要强行猜成某一种。本文程序规模小,plain 的循环顺序本来就连续写 C、连续读 B 的行;blocked 版本增加了三层块循环和边界判断;编译器对两个版本的优化也可能不同。没有性能计数器时,最安全的结论是:分块模型说明了潜在复用结构,但本次小参数真机测量没有出现加速。
反例比漂亮结论更重要。如果实验结果和常见直觉相反,保留反例,降低结论强度,而不是补一个没有证据的解释。
本篇验收
- 能从 64 字节 cache line 手算 AoS/SoA 单字段扫描触碰的 cache line 数。
- 能区分教学 cache 模型输出和真机 wall-clock 输出。
- 能解释为什么 SoA 在本文样本中更快,同时说明固定运行顺序、后台负载和缺少硬件计数器带来的因果边界。
- 能解释 blocked matmul 的工作集手算,并接受本文参数下 blocked 更慢的结果。
- 能复跑 C 基准,并确认坏输入不会触发负数或 huge 参数分配溢出。
练习
练习 1
一个结构体有 4 个 float 字段,大小 16 字节。只扫描第一个字段。cache line 为 64 字节,扫描 8192 个元素。AoS 和 SoA 分别至少触碰多少条 cache line?
答案:
1 | |
练习 2
一次 blocked matmul 的 wall-clock 比 plain 更慢,能否因此断言 cache blocking 没有价值?
答案:不能。只能说明这组参数、代码、编译器和机器状态下没有出现加速。分块是否有价值取决于矩阵尺寸、块大小、cache 层级、编译器优化、访存顺序和测量边界。本文只能陈述:工作集模型显示 32×32 三块为 12288 字节,但当前小参数真机计时 blocked 更慢。






