用 Google Benchmark 实测对比”标量 vs 自动向量化 vs 手动SIMD”三种实现的性能差异?
我来写一个完整的、可直接编译运行的对比实验。
检查编译器和CPU支持的指令集
好,环境支持到 AVX-512。我先确认 Google Benchmark 库是否可用,没有的话用纯 chrono 多次采样的方式实现,避免网络依赖问题。
Ran 2 commands
没有网络访问权限,没法安装 Google Benchmark 库。我改用纯 std::chrono + 多次重复采样取中位数的方式来实现,效果一样能说明问题,而且代码更简洁、不需要额外依赖,你直接复制就能编译运行。
Created a file, ran a command
两个版本都编译成功了。先跑”纯标量基线”(关闭了自动向量化,这是最公平的对照组):
运行纯标量基线测试(关闭自动向量化)
再跑”完整优化版本”(-O3 让编译器自己也尝试自动向量化标量循环):
运行O3完整优化测试,看编译器自动向量化效果
让我反汇编确认一下,验证”标量”版本在 -O3 下是否真的被编译器自动转成了 SIMD 指令——这能解释为什么两个版本在 -O3 下差距很小:
Ran 3 commands
证据找到了——这正好解释了实验结果。
实测结果
| 版本 | 耗时(2000万个float相加) | vs 标量基线 |
|---|---|---|
纯标量(-fno-tree-vectorize) |
15.50 ms | 1.0x |
| 手动 AVX2 SIMD | 10.64 ms | 1.46x |
-O3(编译器自动向量化) |
12.35 ms | 1.25x |
-O3 下的手动 AVX2 |
11.22 ms | 1.38x |
关键发现
1. 验证了文字说的事情——编译器确实”偷偷”做了向量化
我反汇编了 -O3 下”标量版本”的函数,看到了这样的指令:
vmovups (%rdi,%rax), %ymm1 ← 加载256位(8个float)到 ymm 寄存器 vaddps (%rsi,%rax), %ymm1, %ymm0 ← 一条指令完成8个float的加法 vmovups %ymm0, (%rdx,%rax)
ymm 就是 AVX2 的 256 位寄存器,vaddps 是向量加法指令——你写的是普通 for 循环,编译器在 -O3 下自己把它翻译成了和我手写 AVX2 几乎一样的指令。这就是为什么 -O3 下两者差距从 1.46x 缩小到只有 1.10x。
2. 加速比没有达到理论的 8 倍,原因正是上一节提到的:内存带宽瓶颈
这个实验里每个数组 76MB,三个数组共 228MB,远超 CPU 缓存容量(通常几MB到几十MB)。数据要从主内存读取,计算本身(加法)反而不是瓶颈——SIMD 把”计算”加快了 8 倍,但”内存搬运”的速度没变,木桶效应导致整体只提升了 1.4 倍左右。
3. 正确性验证通过——两种实现逐元素比对完全一致,证明手写 SIMD 没有引入精度或逻辑错误。
想验证一下”内存带宽瓶颈”的猜想吗?我可以把数组改小(比如能放进 L2 缓存的几百KB),这样计算会重新成为瓶颈,SIMD 的加速比应该会更接近理论值——这也是实践中判断”问题出在计算还是内存”的标准排查方法。
