i007.cc

i007.cc

优先队列-降维打击

05.价值资料

用 Google Benchmark 实测对比”标量 vs 自动向量化 vs 手动SIMD”三种实现的性能差异?

我来写一个完整的、可直接编译运行的对比实验。

检查编译器和CPU支持的指令集

检查编译器和CPU支持的指令集

好,环境支持到 AVX-512。我先确认 Google Benchmark 库是否可用,没有的话用纯 chrono 多次采样的方式实现,避免网络依赖问题。

Ran 2 commands

Ran 2 commands

没有网络访问权限,没法安装 Google Benchmark 库。我改用std::chrono + 多次重复采样取中位数的方式来实现,效果一样能说明问题,而且代码更简洁、不需要额外依赖,你直接复制就能编译运行。

Created a file, ran a command

Created a file, ran a command

两个版本都编译成功了。先跑”纯标量基线”(关闭了自动向量化,这是最公平的对照组):

运行纯标量基线测试(关闭自动向量化)

运行纯标量基线测试(关闭自动向量化)

再跑”完整优化版本”(-O3 让编译器自己也尝试自动向量化标量循环):

运行O3完整优化测试,看编译器自动向量化效果

运行O3完整优化测试,看编译器自动向量化效果

让我反汇编确认一下,验证”标量”版本在 -O3 下是否真的被编译器自动转成了 SIMD 指令——这能解释为什么两个版本在 -O3 下差距很小:

Ran 3 commands

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 下”标量版本”的函数,看到了这样的指令:

asm
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 的加速比应该会更接近理论值——这也是实践中判断”问题出在计算还是内存”的标准排查方法。

发表回复