特征:编码风格对性能的影响

从我读到的Eigen( 这里 )看来, operator=()似乎是懒惰评估的“障碍” – 例如它会导致Eigen停止返回expression式模板并实际执行(优化的)计算,将结果存储在=的左侧。

这似乎意味着一个人的“编码风格”对性能有影响 – 即使用命名variables来存储中间计算结果可能会对性能产生负面影响,因为计算的某些部分“过早” 。

为了validation我的直觉,我写了一个例子,并对结果感到惊讶( 完整的代码在这里 ):

 using ArrayXf = Eigen::Array <float, Eigen::Dynamic, Eigen::Dynamic>; using ArrayXcf = Eigen::Array <std::complex<float>, Eigen::Dynamic, Eigen::Dynamic>; float test1( const MatrixXcf & mat ) { ArrayXcf arr = mat.array(); ArrayXcf conj = arr.conjugate(); ArrayXcf magc = arr * conj; ArrayXf mag = magc.real(); return mag.sum(); } float test2( const MatrixXcf & mat ) { return ( mat.array() * mat.array().conjugate() ).real().sum(); } float test3( const MatrixXcf & mat ) { ArrayXcf magc = ( mat.array() * mat.array().conjugate() ); ArrayXf mag = magc.real(); return mag.sum(); } 

以上给出了3种不同的计算复数值matrix中幅度系数和的方法。

  1. test1types的计算的每一部分“一步一步”。
  2. test2在一个expression式中完成整个计算。
  3. test3采用了“混合”的方法 – 用一些中间variables。

我有点期待,因为test2将整个计算打包成一个expression式,Eigen将能够利用这一点,并在全局优化整个计算,提供最好的性能。

然而,结果是令人惊讶的(每个testing1000次执行的总数为微秒):

 test1_us: 154994 test2_us: 365231 test3_us: 36613 

(这是用g ++ -O3编译的 – 详细内容请参阅要点 。)

我预计最快的版本( test2 )实际上是最慢的。 另外,我预计最慢的版本( test1 )实际上是在中间。

所以,我的问题是:

  1. 为什么test3比替代schemeperformance得更好?
  2. 有没有一种技术可以使用(短时间潜入汇编代码)来了解Eigen如何实际执行计算?
  3. 是否有一套指导方针可以在特征代码中进行性能和可读性(使用中间variables)之间的良好平衡?

在更复杂的计算中,在一个expression式中执行所有操作可能会妨碍可读性,所以我有兴趣find正确的方式来编写可读性和高性能的代码。

这看起来像GCC的问题。 英特尔编译器提供预期的结果。

 $ g++ -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -oa && ./a test1_us: 200087 test2_us: 320033 test3_us: 44539 $ icpc -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -oa && ./a test1_us: 214537 test2_us: 23022 test3_us: 42099 

icpc版本相比, gcc似乎有优化你的test2问题。

为了获得更精确的结果,您可能需要closures-DNDEBUG的debugging断言,如下所示。

编辑

对于问题1

@ ggael给出了一个很好的答案, gcc失败了向量化sum循环。 我的实验也发现, test2与手写的朴素for-loop一样快,都是使用gccicc ,暗示向量化是原因,在test2用下面提到的方法检测到临时内存分配,这表明Eigen正确评估expression。

对于问题2

避免中间记忆是Eigen使用expression模板的主要目的。 所以Eigen提供了一个macrosEIGEN_RUNTIME_NO_MALLOC和一个简单的函数,使您能够在计算expression式时检查是否分配了中间内存。 你可以在这里find一个示例代码。 请注意,这只能在debugging模式下工作。

EIGEN_RUNTIME_NO_MALLOC – 如果定义了一个新的开关,可以通过调用set_is_malloc_allowed(bool)来打开和closures。 如果malloc不被允许,Eigen试图dynamic分配内存,则会导致断言失败。 没有被默认定义。

对于问题3

有一种方法可以使用中间variables,并同时获得由惰性评估/expression式模板引入的性能改进。

方法是使用具有正确数据types的中间variables。 而不是使用Eigen::Matrix/Array ,它指示expression式被评估,您应该使用expression式typesEigen::MatrixBase/ArrayBase/DenseBase以便expression式只被缓冲但不被评估。 这意味着您应该将expression式存储为中间expression式,而不是expression式的结果,条件是该中间表将仅在以下代码中使用一次。

由于确定expression式Eigen::MatrixBase/...的模板参数可能很痛苦,您可以使用auto 。 你可以在这个页面上find一些关于何时/不应该使用auto / expressiontypes的提示。 另一页也告诉你如何将expression式作为函数parameter passing,而不用评估它们。

根据@ggael的答案中有关.abs2()的指导性实验,我认为另一个方针是避免重新发明轮子。

发生什么是因为.real()步骤,Eigen不会明确地向量化test2 。 因此它会调用标准的complex :: operator *操作符,不幸的是,这个操作符不会被gcc内联。 另一方面,其他版本使用Eigen自己的向量化产品实现的复合体。

相比之下,ICC内联complex :: operator *,因此使得test2成为ICC中最快的。 您也可以将test2重写为:

 return mat.array().abs2().sum(); 

在所有编译器上获得更好的性能:

 gcc: test1_us: 66016 test2_us: 26654 test3_us: 34814 icpc: test1_us: 87225 test2_us: 8274 test3_us: 44598 clang: test1_us: 87543 test2_us: 26891 test3_us: 44617 

在这种情况下,ICC的得分非常高,这是由于其聪明的自动vector化引擎。

在不修改test2情况下解决gcc内联失败的另一种方法是为complex<float>定义自己的operator* 。 例如,在文件顶部添加以下内容:

 namespace std { complex<float> operator*(const complex<float> &a, const complex<float> &b) { return complex<float>(real(a)*real(b) - imag(a)*imag(b), imag(a)*real(b) + real(a)*imag(b)); } } 

然后我得到:

 gcc: test1_us: 69352 test2_us: 28171 test3_us: 36501 icpc: test1_us: 93810 test2_us: 11350 test3_us: 51007 clang: test1_us: 83138 test2_us: 26206 test3_us: 45224 

当然,并不总是推荐这个技巧,因为与glib版本相比,这可能会导致溢出或数字取消问题,但这是icpc和其他vector化版本计算的结果。

我之前做的一件事就是使用auto关键字。 请记住,大多数Eigenexpression式返回特殊expression式数据types(例如CwiseBinaryOp ),返回到Matrix的赋值可能会迫使expression式被评估(这就是您所看到的)。 使用auto允许编译器将返回types推断为任何expression式types,这将尽可能避免评估:

 float test1( const MatrixXcf & mat ) { auto arr = mat.array(); auto conj = arr.conjugate(); auto magc = arr * conj; auto mag = magc.real(); return mag.sum(); } 

这应该基本上接近你的第二个testing用例。 在某些情况下,我已经在保持可读性的同时取得了很好的性能提升(不需要拼出expression式模板types)。 当然,你的里程可能会有所不同,所以仔细的基准:)

我只是想让你注意到,你是以非最优方式进行分析,所以实际上这个问题可能只是你的分析方法。

由于需要考虑caching区域等许多因素,因此应该按照以下方式进行configuration:

 int warmUpCycles = 100; int profileCycles = 1000; // TEST 1 for(int i=0; i<warmUpCycles ; i++) doTest1(); auto tick = std::chrono::steady_clock::now(); for(int i=0; i<profileCycles ; i++) doTest1(); auto tock = std::chrono::steady_clock::now(); test1_us = (std::chrono::duration_cast<std::chrono::microseconds>(tock-tick)).count(); // TEST 2 // TEST 3 

一旦你以正确的方式进行了testing,那么你可以得出结论。

我高度怀疑由于一次只分析一个操作,所以在第三个testing中使用caching版本,因为操作可能会被编译器重新sorting。

另外,你应该尝试不同的编译器,看看问题是否是展开模板(优化模板有一个深度限制:很可能你可以用一个大的expression式来实现)。

另外如果Eigen支持移动语义,没有理由为什么一个版本应该更快,因为并不总是保证expression式可以被优化。

请尝试让我知道,这很有趣。 另外一定要启用像-O3这样的标志优化,没有优化的分析是没有意义的。

为了防止编译器优化所有东西,使用文件或cin初始input,然后重新input函数内部的input。