特征:编码风格对性能的影响
从我读到的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中幅度系数和的方法。
-
test1
types的计算的每一部分“一步一步”。 -
test2
在一个expression式中完成整个计算。 -
test3
采用了“混合”的方法 – 用一些中间variables。
我有点期待,因为test2
将整个计算打包成一个expression式,Eigen将能够利用这一点,并在全局优化整个计算,提供最好的性能。
然而,结果是令人惊讶的(每个testing1000次执行的总数为微秒):
test1_us: 154994 test2_us: 365231 test3_us: 36613
(这是用g ++ -O3编译的 – 详细内容请参阅要点 。)
我预计最快的版本( test2
)实际上是最慢的。 另外,我预计最慢的版本( test1
)实际上是在中间。
所以,我的问题是:
- 为什么
test3
比替代schemeperformance得更好? - 有没有一种技术可以使用(短时间潜入汇编代码)来了解Eigen如何实际执行计算?
- 是否有一套指导方针可以在特征代码中进行性能和可读性(使用中间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一样快,都是使用gcc
和icc
,暗示向量化是原因,在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。