You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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循环仍快于矩阵乘法版本。

原因分析与优化建议

一、矩阵乘法版本更慢的核心原因

  1. 内存布局不匹配
    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指令集能高效批量处理数据。

  2. 额外的中间数据与操作
    矩阵乘法版本会先生成完整的旋转后矩阵,再执行rowwise() += translation,这两步都是独立的内存密集型操作,相当于对3亿个点做了两次完整的内存读写。而for循环版本是旋转+平移一步完成,每个点只做一次读和一次写,内存开销减半。

  3. 矩阵乘法的通用优化冗余
    Eigen的矩阵乘法优化是针对通用矩阵(任意大小)设计的,当其中一个矩阵是3x3的小矩阵时,通用优化的开销会显现出来,不如直接针对Vector3f的专用乘法高效——后者可以被编译器完全展开为无循环的SIMD指令,没有额外的分块、对齐判断等逻辑。

二、代码优化空间

  1. 调整内存布局为列优先的(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循环版本。

  2. 优化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行向量转置的额外操作,让编译器生成更紧凑的代码。

  3. 启用Eigen的显式向量化优化
    在代码开头添加:

    #define EIGEN_DONT_ALIGN_STATICALLY
    #define EIGEN_VECTORIZE_AVX2
    

    配合ICC编译器的-xAVX2选项,强制Eigen利用AVX2指令集做最大化优化,进一步挖掘CPU潜力。

  4. 并行化处理
    3亿个点的循环可以用OpenMP并行化,只需在for循环前加#pragma omp parallel for(确保编译器开启-openmp选项),利用i7-13700k的多核心优势,能进一步降低耗时。

内容的提问来源于stack exchange,提问作者STENoFreedSpeech

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 15:57:04