专栏名称: 计算机视觉工坊
专注于计算机视觉、VSLAM、目标检测、语义分割、自动驾驶、深度学习、AI芯片、产品落地等技术干货及前沿paper分享。这是一个由多个大厂算法研究人员和知名高校博士创立的平台,我们坚持工坊精神,做最有价值的事~
目录
相关文章推荐
新京报书评周刊  ·  从佛教到道教,哪吒如何成为中华文化中的护法神? ·  昨天  
十点读书  ·  成熟女人,会为自己的选择买单 ·  昨天  
新京报书评周刊  ·  英国社会学家麦克·布洛维逝世,享年78岁 ·  5 天前  
51好读  ›  专栏  ›  计算机视觉工坊

Eigen的速度为什么这么快?

计算机视觉工坊  · 公众号  ·  · 2024-09-15 00:00

正文

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

对称圆标定板

如需采购标定板,请联系

扫码可查看商品详情
▲长按添加小助理微信:cv3d007 或者 13451707958,咨询更多
非对称标定板


来自知乎|计算机视觉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






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