为何手动实现的SIMD二维点叉积运算比GCC-O3优化版本慢3倍?
SIMD实现的2D点叉积比普通版本慢3倍的原因及优化方案
我在x86平台尝试用immintrin.h实现SIMD指令加速2D点叉积运算,编写测试代码后,使用Linux平台GCC7.5.0编译器通过命令g++ -o cross_simd cross_simd.cpp -O3 -march=native编译,但性能分析显示SIMD版本的CrossSIMD函数运算速度比普通Cross函数慢3倍,查看汇编后仍未找到原因,求解答。
测试代码
#include <float.h> #include <immintrin.h> #include <cmath> #include <iostream> #include <ctime> // 简化计时实现 class Timer { public: Timer(const std::string& name) : name_(name), start_(clock()) {} ~Timer() { double elapsed = (double)(clock() - start_) / CLOCKS_PER_SEC; std::cout << name_ << " 耗时: " << elapsed << "s\n"; } private: std::string name_; clock_t start_; }; const int kLoop = 10000000; double array[1024]; class Point2 { public: Point2() = default; Point2(double xx, double yy):x_(xx), y_(yy) {} // SIMD版本叉积 inline double CrossSIMD(Point2 other) { __m128d a = _mm_load_pd(&x_); __m128d _other = _mm_set_pd(other.y_, -other.x_); __m128d c = _mm_mul_pd(a, _other); double temp[2]; _mm_store_pd(&temp[0], c); return temp[0] + temp[1]; } // 普通版本叉积 inline double Cross(Point2 other) { return x_* other.y_ - y_ * other.x_; } private: double x_ = 0.; double y_ = 0.; } __attribute__((aligned(16))); int main() { // 初始化测试数组 for(int i=0; i<1024; ++i) array[i] = (double)i; double sum_cross = 0.; { Timer test_1("普通Cross"); for(int i = 0; i < kLoop; ++i) { int index_1x = i & 1023; int index_1y = (i + 1) & 1023; int index_2x = (i + 2) & 1023; int index_2y = (i + 3) & 1023; sum_cross += Point2(array[index_1x], array[index_1y]).Cross(Point2(array[index_2x], array[index_2y])); } } std::cout << sum_cross << std::endl; double sum_simd = 0.; { Timer test_1("SIMD Cross"); for(int i = 0; i < kLoop; ++i) { int index_1x = i & 1023; int index_1y = (i + 1) & 1023; int index_2x = (i + 2) & 1023; int index_2y = (i + 3) & 1023; sum_simd += Point2(array[index_1x], array[index_1y]).CrossSIMD(Point2(array[index_2x], array[index_2y])); } } std::cout << sum_simd << std::endl; std::cout << sum_simd - sum_cross << std::endl; return 0; }
性能变慢的核心原因
- 未利用SIMD批量处理优势:当前
CrossSIMD只是把单个叉积的两个乘法塞进SIMD寄存器,本质还是处理一组数据,反而比普通版本多了SIMD加载、存储、向量构造的额外开销。而O3优化下,普通Cross会被完全内联,编译器用FPU指令生成极高效的运算序列,甚至可能自动做部分向量化。 - 不必要的内存读写:
_mm_store_pd把SIMD结果写入临时数组temp再读取相加,比普通版本的纯寄存器运算多了内存访问开销,即使L1缓存也远不如寄存器操作快。 - 向量构造的额外开销:
_mm_set_pd(other.y_, -other.x_)需要将标量打包到SIMD寄存器,还要对-other.x_做取反,这些操作无法被编译器优化,而普通版本的-y_*other.x_是纯寄存器级运算。 - 手动SIMD打破编译器优化:手写SIMD代码限制了编译器的优化空间,比如无法进行指令重排、寄存器分配优化,而普通版本编译器可以根据CPU特性生成最优指令。
优化方案
1. 去掉内存存储,用SIMD水平相加指令
直接用_mm_hadd_pd(SSE3指令集,GCC7.5支持)完成向量内元素相加,避免临时数组的内存操作:
inline double CrossSIMD(Point2 other) { __m128d a = _mm_load_pd(&x_); __m128d _other = _mm_set_pd(other.y_, -other.x_); __m128d c = _mm_mul_pd(a, _other); // 水平相加向量元素,直接转换为double返回 return _mm_cvtsd_f64(_mm_hadd_pd(c, c)); }
2. 优化向量构造,减少标量转向量开销
直接加载另一个点的坐标,用SIMD指令调整向量元素,避免标量操作:
inline double CrossSIMD(Point2 other) { __m128d a = _mm_load_pd(&x_); // 加载other的x、y到SIMD寄存器 __m128d b = _mm_load_pd(&other.x_); // 交换向量元素,得到 [other.y, other.x] b = _mm_shuffle_pd(b, b, 0x1); // 对低64位元素取反(翻转符号位) const __m128d neg_mask = _mm_set_sd(-0.0); b = _mm_xor_pd(b, neg_mask); __m128d c = _mm_mul_pd(a, b); return _mm_cvtsd_f64(_mm_hadd_pd(c, c)); }
3. 批量处理多组点对(真正发挥SIMD优势)
SIMD的核心是并行处理多组数据,比如一次计算2个叉积,减少循环次数:
// 批量计算两个叉积的SIMD版本 inline __m128d CrossSIMDBatch(const Point2& p1, const Point2& p2, const Point2& p3, const Point2& p4) { // 加载两组点的坐标 __m128d a = _mm_load_pd(&p1.x_); __m128d c = _mm_load_pd(&p3.x_); // 处理第二组点的向量:转为 [y, -x] __m128d b = _mm_load_pd(&p2.x_); b = _mm_shuffle_pd(b, b, 0x1); const __m128d neg_mask = _mm_set_sd(-0.0); b = _mm_xor_pd(b, neg_mask); __m128d d = _mm_load_pd(&p4.x_); d = _mm_shuffle_pd(d, d, 0x1); d = _mm_xor_pd(d, neg_mask); // 同时计算两个叉积 __m128d cross1 = _mm_mul_pd(a, b); __m128d cross2 = _mm_mul_pd(c, d); // 分别相加每个叉积的结果 return _mm_hadd_pd(cross1, cross2); }
主循环中可改为批量处理,每次计算2个叉积,充分利用SIMD并行性。
4. 确保数组对齐
确保测试数组array也是16字节对齐,避免_mm_load_pd的未对齐加载开销:
double array[1024] __attribute__((aligned(16)));
内容的提问来源于stack exchange,提问作者user14503825
相关产品推荐
相关产品推荐

