专栏名称: 3DCV
关注工业3D视觉、SLAM、自动驾驶技术,更专注3D视觉产业的信息传播和产品价值的创造,深度聚焦于3D视觉传感器、SLAM产品,使行业产品快速连接消费者。
目录
相关文章推荐
田俊国讲坛  ·  【3月15日】田俊国老师线下公开课《教学引导 ... ·  15 小时前  
湖北日报  ·  即将开始查分! ·  16 小时前  
湖北日报  ·  即将开始查分! ·  16 小时前  
班主任家园  ·  DeepSeek预测:未来十年内最稳的十大“ ... ·  昨天  
黑龙江省教育厅  ·  《哪吒2》带给我们哪些教育启示? ·  昨天  
黑龙江省教育厅  ·  《哪吒2》带给我们哪些教育启示? ·  昨天  
邳州银杏甲天下  ·  事关2027年高考,省教育厅最新公告 ·  2 天前  
51好读  ›  专栏  ›  3DCV

Eigen的速度为什么这么快?

3DCV  · 公众号  ·  · 2024-09-13 11:00

正文

点击下方卡片 ,关注「3DCV」公众号
选择星标,干货第一时间送达

添加小助理:cv3d008,备注:方向+学校/公司+昵称,拉你入群。文末附3D视觉行业细分群。

扫描下方二维码,加入「3D视觉从入门到精通」知识星球,星球内凝聚了众多3D视觉实战问题,以及各个模块的学习资料:近20门独家秘制视频课程最新顶会论文、计算机视觉书籍优质3D视觉算法源码等。想要入门3D视觉、做项目、搞科研,欢迎扫码加入!


以下内容来自知乎,计算机视觉life整理,如有侵权联系删除https://www.zhihu.com/question/28571059/answer/3057795309
作者 Sisyphus

欢迎来到C++数值优化的世界!Eigen能写的快和它的设计思路有关,涵盖了算法加速的几个方法。Eigen的设计思路,是把所有能优化的步骤放在编译时去优化。要想运行时变快,写Eigen代码的工程师需要显式地告诉Eigen矩阵的特性,告诉的越多,越能提供空间让编译器在编译时加速算法

比如一个矩阵乘法+

# include  Eigen:: Matrixxd Jacobian_i = Eigen:: MatrixXd:: Random(10, 10); Eigen:: Matrixxd Jacobian_j = Eigen:: Matrixxd:: Random(10, 10);
Eigen:: MatrixXd Hessian = Eigen:: Matrixxd:: Zero(10, 10);
Hessian += Jacobian_i. transpose() * Jacobian_j;

这个是一个稠密矩阵相乘。首先需要知道的是Eigen并不是一步一步地先做转置,再去乘,而是使用lazy evaluation的方法。Lazy evalution,是把计算本身尽可能放在最后做,减少内存访问。

在代码上来看,就是transpose()函数并没有做转置,和operator*()本身也没有做乘法,只是更新了一下flag,真正运行计算的是operator+=()。从Eigen/src/Core/EigenBase.h可以看到,是在operator+=()这边才真正去做内存读取和计算。

第二个可以用的优化技巧是改变内存分配的方式,使用Eigen时应该尽可能用静态内存+代替动态内存。在上述矩阵计算中,三个矩阵都是动态内存分配+,因为Eigen::MatrixXd 是如下的缩写:

typedef MatrixXd Matrix

MatrixBase第二和第三个选项是行列的长度,有一项是Dynamic就会用动态内存分配。所以在已知矩阵大小时应尽可能声明大小,比如Matrix 。如果内存在整个程序中大小会变,但知道最大可能的大小,都可以告知Eigen,Eigen同样会选择用静态内存。

# include
Jacobian_i; Eigen:: Matrix« double, Eigen:: Dynamic, Eigen:: Dynamic, Eigen:: ColMajor, 10, 10>
Jacobian_j: Eigen:: Matrixcdouble, Eigen:: Dynamic, Eigen:: Dynamic, Eigen:: ColMajor, 10, 10>
Hessian =
Eigen:: Matrixcdouble, Eigen:: Dynamic, Eigen:: Dynamic, Eigen:: ColMajor, 10, 10>:: Zero();
Hessian += Jacobian_i. transpose()* Jacobian_j;

以上是普通稠密矩阵计算。如果矩阵本身有自身的性质,都可以通知Eigen,让Eigen用对应的加速方式。比如正定矩阵可以只用上三角进行计算,并且在求解时使用Eigen::LLT这样又快又数值稳定的解法,详情见Linear algebra and decompositions。

总之Eigen的高效既来自于算法优化技巧,又来自于工程师本身对公式和内存计算的理解。

参考资料: Eigen源代码+:Files·master:libeigen/eigen

Eigen官方数程:The Matrix class

作者 朱小霖

感兴趣的朋友可以看一下这个pdf,是eigen的主要开发者对其功能和优化进行的总结:

https://download.tuxfamily.org/eigen/eigen_CGLibs_Giugno_Pisa_2013.pdf

这里也根据这个pdf简单说两句。

Eigen的优化主要可以看这个结构图: 从上到下,expression tree是通过c++ template的方法把计算提取成AST一样的东西,并惰性求值,从而减少了对中间变量的需求,这种优化被称为expression template。具体来说,就是对于这样一个计算

A = B + C - D;

按正常的C++,展开直接计算应该类似:

// tmp1 = B + C
Matrix tmp1(n);
for (int i=0; i  tmp1[i] = B[i] + C[i];
// tmp2 = tmp1 - D
Matrix tmp2(n);
for (int i=0; i  tmp2[i] = tmp1[i] - D[i];
// A = tmp2
for (int i=0; i  A[i] = tmp2[i];

而通过抽象成AST,把A和B, C, D通过树连接起来之后再对A的每个元素求值,就可以达到这样的效果:

for (int i=0; i  A[i] = B[i] + C[i] - D[i];

可以很明显对比出两者之间的区别。这里需要注意的是,他的AST用的是C template做的,可能因为Eigen这个库是只有头文件的。以及为了防止重复实现,使用了CRTP这种设计模式。不过这种优化仅限于element-wise的操作,矩阵乘法就不行了。所以eigen对于乘法做了2件事,第一是把常见的乘法操作提取出来,提取为: 单独进行优化。然后因为矩阵乘法需要得到中间变量,所以为了更好的利用expression template,eigen用tree optimizer把矩阵乘法运算往后移。tree optimizer主要就是这样的2条运算顺序调整:

// catch A * B + Y and builds Y' + A' * B'
TreeOpt,Y> > { … };
// catch X + A * B + Y and builds (X'
 + Y') + (A' * B')
TreeOpt >,Y> > { … };

这样就可以把运算

res -= m1 + m2 + m3*m4 + 2*m5 + m6*m7;

调整为

res -= m1 + m2 + 2*m5;
res -= m3*m4;
res -= m6*m7;

注意这其中的每一行可以理解为一个中间变量。

做完tree optimizer,就要开始利用硬件信息优化了,比如做向量化来利用SIMD,进行loop unrolling什么的。对于是否要进行这些优化,eigen对于运算记录了一个经验性的cost model(这里我说经验性的是因为我没看到他利用上了矩阵的形状,所以判断其可能认为所有加法cost都一样,如果有误请大家及时纠正),然后设置了阈值来判断优化是否有益。

还是希望有兴趣的同学去看一下pdf,应该比我这里说的详细很多。以上。

作者 积石息土

看了这里的答案,我也不知道是对还是错,但是我觉得很关键的一点没有人说,那就是向量化 (VECTORIZE)或者SIMD优化,而这正是eigen比你手写的代码快的关键因素。eigen有一个很关键的开关叫做 EIGEN_DONT_VECTORIZE ,默认情况下是关掉的,也就是说eigen在默认情况下是会选择使用simd优化的。我们来做一个测试,对比一下eigen和C语言基础库Morn里面的矩阵运算。Morn是我手写的,可以看做是一个普通程序员手写矩阵运算的正常水平。先看最常见的矩阵乘法+。测试代码如下:

# include "Eigen/Dense"
# include "morn_math.h"

# define T 1000

void test_mul(int K) 
{
  printf("matrix mul, size: %d*%d\n", к,к);
  Eigen:: Matrixxf m_a(K,K); 
  Eigen:: MatrixXf m_b(K,K);
  MMatrix * mat_a = mMatrixCreate(K,K); 
  MMatrix * mat_b = mMatrixCreate(K,к);
  
  for(int j=0;jfor
(int i=0;i  {
    float v; 
    V=mRand(-1000,1000)/1000.8;
    m_a(j,i)=v; 
    mat_a->data[ jl[i]=V; 
    
    V=mRand(-1000,1000)/1000.0;
    m_b(j,i)=v; 
    mat_b->data[ jl[i]=v; 
    }
    Eigen:: MatrixXf m_mul; 
    mTimerBegin("eigen");
    for(int i=0;i      m_mul = m_a*m_b;
      mTimerEnd("eigen");
      
  MMatrix *mat_mul = mMatrixCreate();
  mTimerBegin("Morn");
  for(int i=0;i      mMatrixMul(mat_a,mat_b,mat_ mul);
  mTimerEnd("Morn");
  mMatrixRelease(mat_mul);
  
  mMlatrixRelease(mat_a);
  mMatrixRelease(mat_b);
}
int main()
{
  test_mul(18);
  test_mul(20);
  test_mul(50);
  test_mul(100);
  test_mul(200);
  test_mul(500);
  return 0;
}

以上分别测试了从10×10矩阵到500×500矩阵的乘法。测试时计时1000次。测试使用的编译器如下: 先看,当定义了EIGEN_DONT_VECTORIZE的结果。编译命令为: 测试结果为: 可以看到当EIGEN_DONT_VECTORIZE时,eigen并没有比Morn更快,甚至还比Morn稍慢一点。再测试一下默认的打开向量化时的结果,测试命令为:



测试结果为:

可以看到当使用eigen向量化运算的时候,eigen就明显快于Morn了,尤其对于大规模矩阵运算,有2到4倍的差距。

以上测试足以说明VECTORIZE对Eigen速度的提升效果。那么VECTORIZE到底是什么东西呢。在 eigen的"src/Core/util/ConfigureVectorization.h"文件里。

#if !(defined(EIGEN _DONT_VECTORIZE) Il defined(EIGEN_GPUCC))
  #if defined (EIGEN _SSE2_ON_NON_MSVC _BUT _NOT_OLD_GCC) || defined(EIGEN _SSE2_ON_MSVC_2008_OR_LATER)
    //Defines symbols for compile-time detection of which instructions are
    //used. 
    //EIGEN VECTORIZE YY is defined if and only if the instruction set y is used
    # define EIGEN VECTORIZE
    # define EIGEN VECTORIZE SSE
    # define EIGEN VECTORIZE SSE2

    //Detect sse3/ssse3/sse4:
    //gcc and icc defines _SSE3_, ..
    //there is no way to know about this on msvc. You can define EIGEN VECTORIZE SSE* if you
//want to force the use of those instructions with msvc.
  #ifdef _SSE3_
    #define EIGEN VECTORIZE_SSE3
  #endif
  #ifdef _SSSE3_
    #define EIGEN VECTORIZE_SSSE3
  #endif
  #ifdef _SSE4_1
    #define EIGEN_VECTORIZE_SSE4_1
  #endif
  #ifdef SSE4_2_
    #define EIGEN_VECTORIZE_SSE4_2
  #endif
  #ifdef _AVX_
    #ifndef EIGEN _USE_SYCL
      #define EIGEN VECTORIZE AVX
    #endif
    #define EIGEN_VECTORIZE_SSE3
    #define EIGEN_VECTORIZE_SSSE3
    #define EIGEN_VECTORIZE_SSE4_1
    #define EIGEN_VECTORIZE_SSE4_2
    #endif
    ...
  #endif
  ...
#endif

可以看到eigen的向量化就是使用sse、avx等等simd指令,而且eigen做的很好,已经覆盖了今天所有主流的simd指令集(以上摘抄的只是一小部分)。

但是对于一些不适用于SIMD优化的矩阵运算,eigen就没有这么快了,诸如矩阵转置,它打不打开EIGEN_DONT_VECTORIZE其实影响不大: 其它更多的一些测试:

矩阵求逆:

当eigen不使用向量化的时候:

当eigen使用向量化的时候: 计算行列式:

当eigen不使用向量化的时候:

当eigen使用向量化的时候: 求解线性方程组:

这里使用了eigen的QR分解和LU分解两种方法,Morn里的实现实际为LU分解。

当eigen不使用向量化的时候:


当eigen使用向量化的时候:

所以,结论是eigen实际上整合了各种平台下的各种优化手段(其实不限于SIMD优化,还有诸如GPU加速之类(但是一般人用的时候都默认不打开GPU加速)),做了很多繁杂的工作,因此才达到了优秀的性能表现。

作者 叶洪舟

以前也觉得 Eigen 很快,感觉和 mkl 可以媲美。但是昨天 debug 时一行一行看时间,发现这样一个矩阵乘法 要算将近一秒 ,而同样的计算用 MATLAB 只需要肉眼不可察觉的时间。

我 google 了一下,在 StackOverflow 上

(How to speed up Eigen library's matrix product?)

有人讨论说是 MATLAB 内部自动会调用多线程版的 mkl 里的矩阵乘法,而 Eigen 在通常状态下是单线程的,需要在编译时加上-fopen参数使用 openmp 开启多线程。我在自己的 Mac 上尝试了一下,在线程数为 4 的情况下,时间缩短为 0.57 秒,但相比 MATLAB 的肉眼不可察觉还是有差距。

我对计算机底层的硬件不是很熟,只是最近突然觉得处理起这种可以高度矢量化的问题时,(不是 非常 wisely 地) 使用Eigen 还是没有经过优化的 MATLAB 快。

P.S.: 最开始的单线程版本我使用的是 OS X 自带的 g++ 编译器,优化参数为 -Ofast.测试过加上 -msse2 没有明显区别。这个g++ 实际上是Apple 封装后的 clang++,很可惜的是不支持 openmp.所以为了开启多线程我安装了GNU g++ 编译器。其它编译参数不变。

更新

刚才又上 StackOverflow 上看了别人的几个帖子 (

Eigen vs Matlab: parallelized Matrix-Multiplication

)学习了一下,发现了几个新的 trick:

1.首先之前线程数我设置错误了。我的 CPU physically 应该只有两个核,四线程+是 hyper- thread 的结果,多出来的两个线程并不能加速。我把 OMP_NUM_THREADS 改为 2 以后,速度 变为 左右;2.如果计算矩阵乘法,使用 noalias() 是个不错的 trick,它可以避免生成 temporary 的矩阵存储中间结果。使用这个后缩短为 0.50s 左右;3.对 Eigen 3.3 或以上的版本可以加上 -mavx 和 -mfma 两个参数,进一步缩短为 0.40s 左右。这样总的下来提速了约 30%.

补充

经评论区提醒 Ofast 进行了一些 unsafe+ 的数学优化所以可能牺牲精度,在极端情况下甚至出错。参考如下 stackoverflow 链接的讨论:

stackoverflow.com/quest...

作者 丁一帆

eigen最好的地方不应该是head-only 可以直接扔到cuda里面骚操作嘛?

其他啥matlab做得到嘛-几乎天痛切换到cuda的诱惑比写simd啊avx啊高的不知道哪里去了

(然而windows下eigen 在cuda下面各种报错,我已经看腻了bug fix了)

作者 darren xu

见了好多答主都不开编译优化-_-|||。

VS下选Release,g++下打O3优化。

另外,多测几次取平均,开个10000的for循环,当然写c++时要有些技巧以免for循环也被编译优化 掉。

x86平台上该打的sse或avx优化要打上,arm平台上neon优化要打上。调用矢量化指令的前提是保 证数据内存对齐 (128bit或256bit对齐),否则也不会生效甚至挂掉。

英特尔cpu上matlab的一些矩阵操作大多都调用release版本的mkl库,Eigen也能调用。

至于openmp多线程优化,好像VS自带的1.0版本就是屎,一些操作可能数据同步就会占去大把时 间。在arm上,由于获取释放锁的开销比x86大得多,用了openmp可能会负优化。

作者 Carpathia

感觉Eigen相比于其他线性代数库最突出的特点是,“不是效率高的事情不做,不是最快的算法就不 提供”。

看下Eigen自己说的,

Eigen: What happens inside Eigen, on a simple example

总结来说Eigen做了这么几件事:

1.不用中间变量

2.小矩阵和大矩阵根据实际情况选用

3.特殊的赋值处理

4.矢量化SSE指令

市面上Eigen库最快,对于Armadillo+数学库的组合速度取决于数学库的速度,Armadillo本身数学库,就不用提了。

作者 生死看淡不服就干

Eigen库本身是一个模板类的数值运算库,在做矩阵运算+的时候一般能够取得比较好的结果.但是在 很多时候还有内存相关问题,内存分配的可以通过两种方式来解决:

· 减少动态内存的分配 · 自定义内存池

第一种方式是可以通过注意代码的实现方式,以及使用开发过程中的trick来解决,对原生库没有任何侵入性,对开发者需要相对深入了解Eigen库+本身.本文从这方面入手,详细介绍一些有效的优化手段

·在generalmatrixProcuct的具体实现中

使用了在栈上申请内存的宏定义函数*ei_declare_aligned_stack_constructed_variable.简化一下具体实现如下

#ifdef EIGEN ALLOCA

  #define ei_declare_aligned_stack_constructed_variable+(TYPE,NAME,SIZE,BUFFER)\
    Eigen::internal::check_size_for_overflow+(SIZE);\ 
    TYPE* NAME = (BUFFER)!=0? (BUFFER) \
              : reinterpret_cast+(\
                  (sizeof(TYPE)*SIZE<=EIGEN_STACK_ALLOCATION_LIMIT) ? EIGEN_ALIGNED_ALLOCA(sizeof(TYPE)*SIZE)\
                 Eigen::internal::aligned_malloc+(sizeof(TYPE)*SIZE) ); \     Eigen::internal::aligned_stack_memory_handler EIGEN_CAT(NAME,_stack_memory_destructor+)((BUFFER)==0 ? NAME : 0,SIZE,sizeof(TYPE)*SIZE>EIGEN_STACK_ALLOCATION_LIMIT)             
#else
  #define ei_declare_aligned_stack_constructed_variable(TYPE,NAME,SIZE,BUFFER) \
    Eigen::internal::check_size_for_overflow(SIZE); \
    TYPE* NAME = (BUFFER)!=0 ? BUFFER : reinterpret_cast(Eigen::internal::aligned_malloc(sizeof(TYPE)*SIZE));\
    Eigen::internal::aligned_stack_memory_handler EIGEN_CAT(NAME,_stack_memory_destructor)((BUFFER)==0 ? NAME : 0,SIZE,true)
#endif

也就是说定义了EIGEN ALLOCA的时候使用EIGEN ALLOCA来分配内存,否则的就使用

Eigen :internal :aligned malloc来分配,这个align malloc最终就会去调用std:malloc,所以就会慢

所以没有定义EIGEN ALLOCA就会慢,再看看EIGEN ALLOCA是怎么被定义的 Eigen中定义ALLOCA的方法也很简单,他只考虑了几个平台.没有关于编译器相关的判断,而在GCC中的alloca是有函数

__builtin_alloca来实现的.所以,一些嵌入式的平台把EIGEN_ALLOCA定义为_buildin_alloca就好了.同时需要注意一下,运行的期间不要产生爆栈·

内存

在Eigen使用上,内存相关优化主要有几个方面。

能使用fixed内存的一定用fixed的内存,

//优化前 MatrixXd PHI = Eigen::Matrix:Identity(15, 15);
//优化后 Matrix PHI = Eigen::Matrix: : Identity(15,15);

不能用fixed内存的就一定要用Eigen::Map,使用预分配的内存来做操作.

//优化前  MatrixXd~PHt(M,NDim);
//优化后  std::vector^*memPHt;  memPHt_.resize(MAXSIZE);//初始化时,分配vector内存
Eigen::Map<:matrixxd> PHt(memPHt_.data(),M,NDim);//动态matrix*使用vector的内存

Noalias: noalias t指的是明确知道两块内存没有重叠的时候,这样就可以用noalias来告诉eigent,当然eigen也只有对矩阵乘法或者是dst大小会被改变的时候才会去做noalias检查,因此也不需要写过多的noalias

Eigen:: MatrixXd mA(N, N); 
Eigen:: MatrixXd mB(N, N); 
Eigen:: MatrixXd mC(N, N); 
Eigen:: MatrixXd mD(N, N);
mD. noalias() = mA * mB * mC;

Block的引用,一定要注意block是不能直接引用的

对block的引用会创建一个reference-like的对象,这样又会有内存分配...

小矩阵乘法

在实际开发过程中,偶然发现2x6*6x100之类的矩阵运算在某些平台系统会非常慢且速度不稳定,比在同类arm平台上慢10倍.

调试发现结果是因为小矩阵也走到了GEMP,P就是对数据进行pack,简单理解就是给block中的数据顺序给理顺再进行计算,这样在无形中又增加了开销 所以造成了小矩阵慢,在stackoverflow上也有人有同样的结论

因此对小矩阵乘法,我们就用手写矩阵相乘就好了,也不用别的技巧,交给编译器来优化就ok了.

在笔者用的算法中这部分大约会做50次,每次三个2x6x6x100的矩阵相乘,按照GEMM的FLOPS计算方式也就是2x MxNxK次运算,在这里就一共是50x3x2x6x100 = 180000,花费了0.3ms左右,转化为FLOPS也就是600000000次/s,也就是0.6GFLOPS,我们CPU现在限制在1.1GHZ,FMA在一个时钟周期最多能运行两条,这样最高也就是2.2GFLOPS.因此可以看到基本上这个速度也是在正常范围内了,,在优化前,这一步的耗时基本在2-3ms左右,最慢还能到4ms...

调试

EIGEN NO_MALLOC

用来检查使用eigen的过程中有没有动态内存的分配,打开这个宏,很容易我们就能发现block的引用之类的,还有一些赋值操作,都有产生内存的分配.这样,我们就可以具体的来解决长尾的内存问题

Benckmark

有很多细小的操作,并不能确定哪种方式来的快,这样就需要用google benckmark的库来做辅助,把小的demo从大的工程中拆解出来,放到google benckmark中来进行测试,这样就能解决很多细小的问题.这里推荐一个简单写benchmark的工具 quick-bench.com/







请到「今天看啥」查看全文