Eigen矩阵乘法比for循环慢3倍?求原因及优化方案
#include <Eigen/Dense> #include <iostream> #include <random> #include <chrono> int main() { const int num_points = 300000000; Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic> init_points(num_points, 3); Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic> transformed_points(num_points, 3); std::mt19937 rng(42); std::uniform_real_distribution<float> dist(-100.0f, 100.0f); for (int i = 0; i < num_points; ++i) { init_points(i, 0) = dist(rng); // x init_points(i, 1) = dist(rng); // y init_points(i, 2) = dist(rng); // z } float theta = 3.14159265358 / 4; // pi/4 Eigen::Matrix3f rotation; rotation = Eigen::AngleAxisf(theta, Eigen::Vector3f::UnitZ()); Eigen::Vector3f translation(10.0f, 20.0f, 30.0f); auto start_time = std::chrono::high_resolution_clock::now(); //transformed_points = init_points * rotation; //uncomment this line to use the Matrix multiply version //transformed_points.rowwise() += translation.transpose(); //uncomment this line to use the Matrix multiply version for (int i = 0; i < num_points; ++i) //comment this for loop to use the Matrix multiply version { Eigen::Vector3f v = init_points.row(i).transpose(); v = rotation * v; v += translation; transformed_points.row(i) = v.transpose(); } auto end_time = std::chrono::high_resolution_clock::now(); std::chrono::duration<double, std::milli> duration_ms = end_time - start_time; std::cout << "total consume: " << duration_ms.count() << "ms" << std::endl; std::cout << "first 5 points:(x,y,z)" << std::endl; for (int i = 0; i < 5; ++i) { std::cout << "("<< transformed_points(i, 0) << ","<< transformed_points(i, 1) << ", " << transformed_points(i, 2) << ")" << std::endl; } return 0; }
我正在对采样获取的点云对象执行坐标变换,需先完成点集旋转再平移,得到变换后的点云。该点云共含3亿个点,采用Eigen动态数组存储。
上述测试代码包含for循环逐点处理和Eigen矩阵乘法两种实现,切换方式为注释/取消对应代码块。在i7-13700k CPU、开启Intel ICC编译器SIMD及AVX优化的环境下,两种版本耗时差异显著:
- 矩阵乘法+逐行平移版本:约2044ms
- for循环逐点旋转平移版本:仅约600ms
已知Eigen会利用CPU指令集优化矩阵乘法,为何此处for循环反而快3倍?此外代码是否还有优化空间?恳请分析解答。
PS:已开启编译器优化,此处指开启优化后for循环仍快于矩阵乘法版本。
一、矩阵乘法版本更慢的核心原因
内存布局不匹配
Eigen默认采用列优先存储,你的init_points是(3亿行,3列)的矩阵,转置后才是点云常用的(3, N)列向量布局。矩阵乘法init_points * rotation实际是按列优先计算,每个点的三个分量在内存中是分散的(3亿行的同一列连续存储),导致CPU缓存命中率极低——处理一个点需要跨3亿行取x/y/z,完全无法利用空间局部性,大量时间浪费在内存等待上。
而for循环版本中,init_points.row(i)虽然是行访问,但编译器会优化为连续读取内存中的三个相邻元素(因为你是逐行遍历,内存访问模式是连续的:x0,y0,z0,x1,y1,z1...),缓存命中率极高,AVX指令集能高效批量处理数据。额外的中间数据与操作
矩阵乘法版本会先生成完整的旋转后矩阵,再执行rowwise() += translation,这两步都是独立的内存密集型操作,相当于对3亿个点做了两次完整的内存读写。而for循环版本是旋转+平移一步完成,每个点只做一次读和一次写,内存开销减半。矩阵乘法的通用优化冗余
Eigen的矩阵乘法优化是针对通用矩阵(任意大小)设计的,当其中一个矩阵是3x3的小矩阵时,通用优化的开销会显现出来,不如直接针对Vector3f的专用乘法高效——后者可以被编译器完全展开为无循环的SIMD指令,没有额外的分块、对齐判断等逻辑。
二、代码优化空间
调整内存布局为列优先的(3, N)格式
把点云存储为Eigen::Matrix<float, 3, Eigen::Dynamic>,这样每个点的x/y/z是连续的列向量,矩阵乘法rotation * init_points会直接利用Eigen的SIMD优化,同时平移可以用colwise() += translation,效率会大幅提升。修改后代码示例:Eigen::Matrix<float, 3, Eigen::Dynamic> init_points(3, num_points); Eigen::Matrix<float, 3, Eigen::Dynamic> transformed_points(3, num_points); // 初始化时按列赋值 for (int i = 0; i < num_points; ++i) { init_points(0, i) = dist(rng); init_points(1, i) = dist(rng); init_points(2, i) = dist(rng); } // 变换 transformed_points = rotation * init_points; transformed_points.colwise() += translation;这种布局下,矩阵乘法的内存访问是连续的,缓存命中率拉满,性能会接近甚至超过原for循环版本。
优化for循环的内存访问
原for循环中init_points.row(i).transpose()和transformed_points.row(i) = v.transpose()会有不必要的转置操作,可以直接用Map来避免:for (int i = 0; i < num_points; ++i) { Eigen::Vector3f v = Eigen::Map<const Eigen::Vector3f>(&init_points(i, 0)); v = rotation * v + translation; Eigen::Map<Eigen::Vector3f>(&transformed_points(i, 0)) = v; }这样跳过了Eigen行向量转置的额外操作,让编译器生成更紧凑的代码。
启用Eigen的显式向量化优化
在代码开头添加:#define EIGEN_DONT_ALIGN_STATICALLY #define EIGEN_VECTORIZE_AVX2配合ICC编译器的
-xAVX2选项,强制Eigen利用AVX2指令集做最大化优化,进一步挖掘CPU潜力。并行化处理
3亿个点的循环可以用OpenMP并行化,只需在for循环前加#pragma omp parallel for(确保编译器开启-openmp选项),利用i7-13700k的多核心优势,能进一步降低耗时。
内容的提问来源于stack exchange,提问作者STENoFreedSpeech

