点击下方卡片
,关注「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 = tmp2for (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/