为何Eigen::VectorXd的operator+=比std::vector循环相加慢约69%?
以下代码需依赖google benchmark,实现填充两个向量并执行相加操作,结果存储在第一个向量中。我分别使用Eigen::VectorXd和std::vector作为向量类型进行性能对比:
#include <Eigen/Core> #include <benchmark/benchmark.h> #include <vector> auto constexpr N = 1024u; template <typename TVector> TVector generate(unsigned min) { TVector v(N); for (unsigned i = 0; i < N; ++i) v[i] = static_cast<double>(min + i); return v; } auto ev1 = generate<Eigen::VectorXd>(0); auto ev2 = generate<Eigen::VectorXd>(N); auto sv1 = generate<std::vector<double>>(0); auto sv2 = generate<std::vector<double>>(N); void add_vectors(Eigen::VectorXd& v1, Eigen::VectorXd const& v2) { v1 += v2; } void add_vectors(std::vector<double>& v1, std::vector<double> const& v2) { for (unsigned i = 0; i < N; ++i) v1[i] += v2[i]; } static void eigen(benchmark::State& state) { for (auto _ : state) { add_vectors(ev1, ev2); benchmark::DoNotOptimize(ev1); } } static void standard(benchmark::State& state) { for (auto _ : state) { add_vectors(sv1, sv2); benchmark::DoNotOptimize(sv1); } } BENCHMARK(standard); BENCHMARK(eigen);
测试运行在Intel Xeon E-2286M @2.40Ghz处理器上,使用Eigen 3.3.9版本、MSVC 16.11.2编译器,启用的相关编译选项包括/GL、/Gy、/O2、/D "NDEBUG"、/Oi、/arch:AVX,典型的运行输出如下:
Run on (16 X 2400 MHz CPU s) CPU Caches: L1 Data 32K (x8) L1 Instruction 32K (x8) L2 Unified 262K (x8) L3 Unified 16777K (x1) -------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------- standard 99 ns 100 ns 7466667 eigen 169 ns 169 ns 4072727
结果显示std::vector的相加操作比Eigen::VectorXd快约69%。二者的核心循环汇编代码如下:
// For Eigen::VectorXd 00007FF672221A11 vmovupd ymm0,ymmword ptr [rcx+rax*8] 00007FF672221A16 vaddpd ymm1,ymm0,ymmword ptr [r8+rax*8] 00007FF672221A1C vmovupd ymmword ptr [r8+rax*8],ymm1 00007FF672221A22 add rax,4 00007FF672221A26 cmp rax,rdx 00007FF672221A29 jge eigen+0C7h (07FF672221A37h) 00007FF672221A2B mov rcx,qword ptr [rsp+48h] 00007FF672221A30 mov r8,qword ptr [rsp+58h] 00007FF672221A35 jmp eigen+0A1h (07FF672221A11h) // For std::vector 00007FF672221B40 vmovups ymm1,ymmword ptr [rax+rdx-20h] 00007FF672221B46 vaddpd ymm1,ymm1,ymmword ptr [rax+rcx-20h] 00007FF672221B4C vmovups ymmword ptr [rax+rcx-20h],ymm1 00007FF672221B52 vmovups ymm1,ymmword ptr [rax+rdx] 00007FF672221B57 vaddpd ymm1,ymm1,ymmword ptr [rax+rcx] 00007FF672221B5C vmovups ymmword ptr [rax+rcx],ymm1 00007FF672221B61 lea rax,[rax+40h] 00007FF672221B65 sub r8,1 00007FF672221B69 jne standard+0C0h (07FF672221B40h)
可以看到二者均使用vaddpd指令一次完成4个double类型数值的相加,但std::vector对应的循环被编译器展开,每次迭代执行2次vaddpd,而Eigen对应的循环没有被展开;另一个潜在差异是std::vector的循环地址按32字节对齐。
我添加/Qvec-report:2编译选项后得到如下提示:
[...]\Core\AssignEvaluator.h(415) : info C5002: loop not vectorized due to reason '1305'
查阅微软官方文档可知,错误码1305的含义是“类型信息不足”。我猜测是Eigen内部手动使用_mm256_add_pd等内建函数的优化逻辑干扰了编译器,反而影响了性能,直接让编译器做自动向量化效果更好。请问我是否有遗漏的优化点?该问题是否属于Eigen的优化缺失bug?
问题解答
遗漏的优化点
你确实有几个Eigen相关的优化配置没有启用:
- 开启内存对齐定义:添加编译参数
/D EIGEN_MAX_ALIGN_BYTES=32,保证Eigen的堆分配内存按AVX要求的32字节对齐,优化后Eigen的内存访问指令可以从非对齐的vmovupd换成对齐的vmovapd,降低内存访问开销。 - 按需禁用Eigen内置向量化:添加编译参数
/D EIGEN_DONT_VECTORIZE,关闭Eigen内部手写的SIMD实现,完全交给MSVC做自动向量化和循环展开,在你这个简单的向量加法场景下,编译器的激进优化效果会优于Eigen的跨平台通用SIMD实现。 - 适配固定长度场景:你的测试用例使用固定长度1024的向量,可以把
Eigen::VectorXd换成固定大小的Eigen::Matrix<double, 1024, 1>,编译器可以获取完整的长度信息,做更极致的循环展开优化。
是否属于Eigen的优化bug
该结果不属于Eigen的优化缺失bug:
- Eigen的核心设计目标是覆盖通用矩阵运算场景,尤其是大尺寸矩阵、多操作链式运算的场景,手写SIMD逻辑的核心作用是保证跨GCC、Clang、MSVC等不同编译器的性能下限,针对你这种单一的小尺寸向量加法的极端场景,限制了特定编译器的激进优化空间属于设计取舍,不是缺陷。
- 你使用的MSVC 2019版本自动向量化能力已经有较大提升,简单单层循环的自动优化效果超过Eigen的通用SIMD实现属于正常现象,相同代码在GCC或Clang下测试,大概率会出现Eigen性能反超的结果。
- Eigen 3.4及以上版本已经优化了对MSVC编译器的向量化适配,升级版本后重新测试,二者的性能差距会大幅缩小。
内容的提问来源于stack exchange,提问作者Cassio Neri
相关产品推荐
相关产品推荐

