☰
CPU性能工程实测:SoA比AoS快14倍——SIMD数据布局的完整规则
2026/10/15 20:56:39 网站建设 项目流程

【免费下载链接】cpu-performance-engineering

A reading path for CPU performance engineering, from one instruction to production inference. Primary sources only, with a runnable benchmark for every section.

项目地址:https://gitcode.com/gh_mirrors/cp/cpu-performance-engineering
点击查看免费下载

cpu-performance-engineering 项目用一组可复现的 CPU 性能基准实测了数据布局对 SIMD 的影响:同样的 419 万条记录,按数组中的结构体(AoS)存储比按结构体中的数组(SoA)存储慢 14.23 倍。本文把实测数字、生成代码和适用边界完整讲清楚,帮你掌握 SIMD 数据布局的判断规则。

两种数据布局:同一份数据的两种存法

数据布局决定循环要搬运的字节数——这是本基准的全部论点。下面是最小化的例子:每条记录 16 个 float(64 字节),求第 3 个字段的和。

// AoS:一个 64 字节结构体的数组 typedef struct { float f[16]; } rec_t; float s = 0.0f; for (size_t i = 0; i < n; i++) s += aos[i].f[3]; // 每读 4 字节,拖进整条缓存行 // SoA:16 个独立的 float 数组 float t = 0.0f; for (size_t i = 0; i < n; i++) t += f3[i]; // 每读 4 字节,只取 4 字节

两种布局的 ASCII 对照:

AoS: [a0 b0 c0 ... p0] [a1 b1 c1 ... p1] [a2 b2 c2 ... p2] ← 取 a 字段要整行读入 SoA: a0 a1 a2 a3 ... b0 b1 b2 b3 ... p0 p1 p2 p3 ...

在 AoS 中,只要读一条记录的任意一个字段,CPU 就要把承载它的整条缓存行搬进来;在 M4 上缓存行是 128 字节,一条行里装着两条完整记录。求和只需要其中 4 字节的字段,另外 60 字节白白占带宽。这就是 SoA 比 AoS 快 14 倍的物理来源。

基准是怎么测的:方法要点

  • 数据:4194304 条记录 × 16 个 float,两种布局各存一份。AoS 共 256 MiB,SoA 每个字段数组 16 MiB(见 bench.c)。
  • 机器:Apple M4 Pro 单线程跑在性能核(P-core)上,无 SMT,估计时钟约 4.50 GHz。
  • 内核:两个——sum3(求第 3 个字段的和,每记录读 4 字节)和dot37(第 3 字段 × 第 7 字段之和,每记录读 8 字节)。
  • 冷缓存:每轮计时前先用 128 MiB 缓冲区扫一遍刷掉缓存,保证数据从内存流式读入,而不是靠缓存取胜。
  • 统计:31 轮计时 + 1 轮预热,报告中位数与变异系数(cv),每轮用独立采样的时钟换算成周期数(见 run.sh 与 timing.h)。
  • 校验:每个变体结果都与 double 精度参考值比对,偏差超 1e-2 即失败运行。

完整测量说明见 基准目录,机器描述和原始输出在 raw.txt,摘要在 summary.md。

实测结果:14.23 倍与 10.63 倍

M4 Pro、-O2、中位数计时,每记录耗时与有效带宽:

内核布局/形式每记录字节中位 ns/rec有效 GB/s相对 AoS 标量
sum3AoS 标量640.668595.71×
sum3AoS -O2640.6247102.41.07×
sum3SoA 标量40.55527.21.20×
sum3SoA 严格顺序40.47378.41.41×
sum3SoA -O240.043991.114.23×
sum3SoA NEON intrinsics40.045687.713.70×
dot37AoS 标量640.787181.31×
dot37SoA -O280.0619129.310.63×

三个关键对比(完整 18 行比率表见 README):

  1. 布局 × 向量化 = 14.23×:sum3的aos_o2对soa_o2是 0.6247 对 0.0439 ns/rec。字节比恰好是 64:4 = 16,时间比 14.23 与之接近但不相等——因为两种布局的有效带宽不同(102.4 对 91.1 GB/s),都跑到了单核能从内存拉取带宽的上限附近,时间比就是字节比乘以各自能跑到的带宽差。
  2. 乘积内核 = 10.63×:dot37要读两个字段,SoA 每记录 8 字节,字节比 64:8 = 8;时间比 10.63 超过 8,因为 SoA 两路字段流合起来拉到了 129.3 GB/s,而 AoS 单路结构体流只有 97.2 GB/s。
  3. 标量下布局几乎没差:aos_scalar对soa_scalar只有 1.20× 和 1.03×。单链浮点归约受fadd/fmadd依赖链延迟约束,每轮只处理一条记录,带宽差异被延迟掩盖——直到编译器把循环真正向量化,布局的差距才一下子放大成 14 倍。

生成代码:编译器对两种布局做了什么不同

AoS 的"向量化"是 gather。-O2下 clang 确实把结构体求和循环标成"vectorized",但生成的是逐 lane 的 gather:每个 4-lane 寄存器由标量ldr和三个间隔 64 字节的ld1.s单 lane 插入填满,16 条记录要 16 次加载,循环每轮前进 1024 字节。结构体乘积(dot37)则被成本模型直接拒绝向量化("the cost-model indicates that vectorization is not beneficial"),只交织成 4 条标量fmadd链。两种情况都改变不了一个事实:每条记录整条缓存行都要进来,字节数不变。

SoA 是真正的全宽向量。同一-O2,字段数组循环是两条 32 字节ldp加四条独立的 4-lanefadd.4s,每轮 16 条记录,累加器之间没有依赖链(汇编对照见 README):

_sum3_soa_o2: ldp q4, q5, [x9, #-32] ; 一条指令 32 字节,8 条记录 ldp q6, q7, [x9], #64 fadd.4s v0, v0, v4 ; 4 个独立向量累加器 fadd.4s v1, v1, v5 fadd.4s v2, v2, v6 fadd.4s v3, v3, v7 subs x10, x10, #16 b.ne LBB4_7

关键陷阱:编译器说"已矢量化"不等于变快

没有授权重排时(严格 IEEE 顺序,即读者默认拿到的编译),clang 依然把 SoA 循环报成 "vectorized loop (width 4)"——但加法被拆回逐 lane 的标量fadd串行链:加载是宽的,算术是串的。实测soa_strict只有 0.4737 ns/rec,比soa_o2慢 10.79 倍。

所以真正的解锁开关不是布局本身,而是允许编译器重排浮点加法顺序:clang 用作用域化的#pragma clang fp reassociate(on),其它编译器用-fassociative-math或更宽的-ffast-math。重排许可让 4 个独立累加器并行工作,把 0.4737 拉到 0.0439。布局决定能搬多少字节,重排许可决定能不能用 SIMD 宽度去搬——两个条件同时满足,14 倍才出现。

顺带一个反直觉的精度细节:单链求和的累积误差反而更大(相对误差约 1.9e-3),而 4 累加器的向量形式把部分和缩小 16 倍,误差压到 9.57e-6 以内——向量化在这里同时赢了速度和精度。

什么情况下 SoA 真的值得:三条判断规则

规则 1:只读大记录里的一小部分字段,且数据超出 L2,才考虑 SoA。每记录只读 4 字段的求和,AoS 要搬 64 字节;读 2 字段的乘积要搬 64 对 8。反之,如果循环要读整条记录、记录本身小、或数据能装进 L2,布局差异会缩小甚至消失。

规则 2:SIMD 收益的前提是允许重排。对浮点归约,没有重排许可就没有多累加器,vectorizedremark 只是描述加载,不代表算术被向量化。用-Rpass=loop-vectorize一类的优化报告(-Rpass-missed=loop-vectorize能告诉你哪个循环被放弃及原因)去核对编译器为什么没提速。

规则 3:验证手段是看汇编 + 实测,不是听 remark。本基准里aos_o2的 sum 循环报"vectorized"却只比标量快 1.07 倍;soa_strict同样报"vectorized"却慢 10.79 倍。remark 描述的是加载(和乘法),不是加法链。

适用边界(读数字前必看):

  • 本机是 Apple M4 Pro,128 字节缓存行、单核约 80–130 GB/s 带宽。x86-64 上缓存行 64 字节、单核带宽更低,AoS 的劣势会被进一步放大。
  • x86 的 clang 可能给 AoS 求和发真正的 gather 指令,向量形式变成 AVX2/AVX-512,汇编需要重新看。
  • GCC 没有作用域化的重排 pragma:GCC 下soa_o2等同soa_strict,整单元开-ffast-math或-fassociative-math才能拿到向量收益。
  • macOS 无用户态 PMU,"搬了多少字节"是从布局和缓存行推出来的,不是数出来的。

自己动手复现:一条命令约 4 秒

在基准目录下直接跑:

./run.sh # 完整运行:1<<22 条记录 × 31 轮 QUICK=1 ./run.sh # 冒烟测试:1<<19 条记录 × 5 轮 REPS=51 ./run.sh # 更多计时轮次

run.sh会先编译(build.sh)、记录机器信息与时钟估算、跑完计时、生成 raw.txt 与 summary.md,并刷新 README 里的结果表。编译脚本在 compile_all.sh,计时与统计工具在 timing.h,时钟估算器在 clock_estimate.c。

配套资料

  • 基准目录(源码 + 结果 + 方法):misc/benchmarks/07-aos-vs-soa-simd/
  • 基准源码(两种布局、六个变体):bench.c
  • 完整结果与方法说明:README.md
  • 相关基准(缓存延迟阶梯):misc/benchmarks/04-cache-latency/
  • 相关基准(自动向量化与别名):misc/benchmarks/08-autovectorization-aliasing/
  • 数据布局一手文献清单(第 7 章笔记):misc/notes/sections/07-single-thread.md
  • 全部 14 个可运行基准索引:misc/benchmarks/
  • 项目入口:README.md

cpu-performance-engineering 的每个章节都配一个可运行基准,从一条指令讲到生产级 CPU 推理,只收一手资料。这篇"14 倍"背后的完整方法学——冷缓存、中位数、每轮独立时钟采样——就是该项目衡量任何数字的标配。

【免费下载链接】cpu-performance-engineering

A reading path for CPU performance engineering, from one instruction to production inference. Primary sources only, with a runnable benchmark for every section.

项目地址:https://gitcode.com/gh_mirrors/cp/cpu-performance-engineering
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询