为何SSE intrinsics代码运行速度仅略快于普通C++实现
SSE手写复数乘法相比常规C++实现性能提升不足的原因分析
问题背景
编写测试代码对比常规C++实现与SSE intrinsics实现的双精度复数乘累加运算运行性能,观测到两类代码的运行耗时差异仅在5%-10%区间,未达到预期的显著性能差距。
测试配套材料
- 源码清单:
main.cpp:主程序,实现双精度复数数组的大循环乘累加逻辑,使用_mm_malloc分配对齐内存prods.cpp:复数乘法实现文件,包含两个版本的运算逻辑:- 基于
std::complex<double>的标准共轭乘法prod - 基于
__m128d的手写SSE复数乘法prod_sse,调用了_mm_shuffle_pd、_mm_mul_pd、_mm_fmsubadd_pd等intrinsics接口
- 基于
Makefile:工程编译配置文件
- 编译配置:使用g++编译,基础参数为
--std=c++20 -march=native -O2;额外添加-flto开启链接时优化后,两类代码的性能差异仍维持在5%-10%区间 - 反汇编初步观测结果:
- 非SSE版本的内存
mov指令数量是SSE版本的两倍 - 独立编译的普通
prod函数运算指令更多:分别独立处理实部、虚部计算,包含非数值异常分支,异常触发时会调用__muldc3库函数 - SSE版本的
prod_sse函数指令更精简,使用打包双精度运算指令,但实际运行性能未达到预期差距
- 非SSE版本的内存
核心成因说明
- 编译器跨函数优化抵消了手写SSE的指令优势
你观测到的带异常分支、调用__muldc3的prod反汇编代码,是未做内联优化的独立函数版本。在-O2 -flto -march=native配置下,编译器会对循环内的prod调用做内联展开:一方面会直接裁掉NaN/inf触发的异常冷分支,不会在热路径中做分支判断或库函数调用;另一方面会自动识别连续复数数组的对齐内存访问模式,对标量实部虚部计算做自动向量化,生成和手写SSE逻辑高度重合的打包双精度运算指令,两者实际在循环热路径执行的指令序列差异极小。 - 负载属于内存绑定型,计算端优化被内存耗时掩盖
大数组双精度复数乘累加是典型的内存带宽受限负载:每个双精度复数占16字节,遍历数组时CPU执行浮点运算的速度远高于从L3缓存/主存加载数据的速度,计算端哪怕指令数相差一倍,产生的耗时差也会被内存等待的开销完全覆盖。你观测到的5%-10%性能差异,仅来自寄存器分配、指令调度的微小区别,并非计算吞吐量的本质差距。 - 当前SSE实现的实际并行度未达到预期
你使用128位SSE寄存器实现的prod_sse,仅把单个复数的实部、虚部打包到同一个寄存器做运算,本质上是单复数的标量计算打包,没有实现「单条指令同时处理多个复数」的SIMD并行——这种实现方式相对于标量计算的理论性能上限本身就不足2倍,再叠加编译器自动向量化的优化效果,实际收益自然远低于预期。如果要获得显著性能提升,需要使用256位AVX/512位AVX-512指令集,一次加载2~4个复数做批量运算,同时配合循环展开填满CPU的浮点执行端口。 - 静态指令数差异不代表实际执行周期差异
现代x86 CPU是乱序超标量架构,每个时钟周期可以并行派发3~4条不同类型的指令到独立执行端口运行。哪怕标量版本的mov指令数量多一倍,只要这些内存加载指令可以和浮点计算指令在不同端口并行执行,就不会带来额外的执行耗时。你看到的标量版本独立计算实部、虚部的指令序列,CPU可以在同一周期内调度到不同端口并行完成,总执行周期和打包SSE指令的差距非常小。
内容的提问来源于stack exchange,提问作者Misora Grilo
相关产品推荐
相关产品推荐

