从我读过的有关Eigen(here)的内容来看,似乎operator=()
充当了一个"屏障"各种各样的懒惰评估 - 例如它导致Eigen停止返回表达式模板并实际执行(优化)计算,将结果存储到=
的左侧。
这似乎意味着一种编码风格"对性能产生影响 - 即使用命名变量来存储中间计算的结果可能会对计算的某些部分进行评估并过早地对性能产生负面影响。
为了验证我的直觉,我写了一个例子并对结果感到惊讶(full code here):
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种不同的方法来计算复值矩阵中的系数方向的和。
test1
对计算的每个部分进行处理,一次一步。&#34; test2
在一个表达式中完成整个计算。test3
采用&#34;混合&#34;方法 - 有一些中间变量。我有点期望,因为test2
将整个计算打包成一个表达式,Eigen将能够利用它并全局优化整个计算,从而提供最佳性能。
然而,结果令人惊讶(显示的数字是每次测试1000次执行的总微秒数):
test1_us: 154994
test2_us: 365231
test3_us: 36613
(这是用g ++ -O3编译的 - 有关详细信息,请参阅the gist。)
我期望最快的版本(test2
)实际上是最慢的。此外,我期望最慢的版本(test1
)实际上在中间。
所以,我的问题是:
test3
的表现比其他选择更好?在更复杂的计算中,在一个表达式中执行所有操作可能会妨碍可读性,因此我有兴趣找到编写既可读又高效的代码的正确方法。
答案 0 :(得分:14)
这看起来像海湾合作委员会的问题。英特尔编译器给出了预期的结果。
$ g++ -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a
test1_us: 200087
test2_us: 320033
test3_us: 44539
$ icpc -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a
test1_us: 214537
test2_us: 23022
test3_us: 42099
与icpc
版本相比,gcc
似乎在优化您的test2
时出现问题。
为了获得更精确的结果,您可能需要按-DNDEBUG
gcc
关闭调试断言。
修改强>
问题1
@ggael给出了一个很好的答案,test2
无法对sum循环进行矢量化。我的实验还发现gcc
和手写的朴素for循环一样快,都有icc
和test2
,这表明矢量化是原因,没有临时内存分配通过下面提到的方法在Eigen::Matrix/Array
中检测到,表明Eigen正确地评估了表达式。
问题2
避免使用中间存储器是Eigen使用表达式模板的主要目的。因此,Eigen提供了一个宏here和一个简单的函数,使您可以检查在计算表达式期间是否分配了中间内存。您可以找到示例代码EIGEN_RUNTIME_NO_MALLOC。请注意,这可能仅适用于调试模式。
EIGEN_RUNTIME_NO_MALLOC - 如果已定义,则引入新的开关 可以通过调用set_is_malloc_allowed(bool)打开和关闭。如果 不允许使用malloc,Eigen会尝试动态分配内存 无论如何,断言失败的结果。默认情况下未定义。
问题3
有一种方法可以使用中间变量,同时获得延迟评估/表达模板引入的性能改进。
方法是使用具有正确数据类型的中间变量。您应该使用表达式Eigen::MatrixBase/ArrayBase/DenseBase
,而不是使用指示要计算表达式的Eigen::MatrixBase/...
,以便表达式仅缓冲但不进行求值。这意味着您应该将表达式存储为中间,而不是表达式的结果,条件是此中间体仅在以下代码中使用一次。
由于确定表达式类型auto
中的模板参数可能会很痛苦,您可以使用auto
代替。您可以在here中找到关于何时应该/不应该使用.abs2()
/表达式类型的一些提示。 this page还告诉您如何将表达式作为函数参数传递而不评估它们。
根据@ggael答案中关于SELECt rb.resource_id, re.code,rb.TYPE,
(
SELECT i18n_resourcebase.NAME
FROM i18n_resourcebase
WHERE language_id=1
AND (i18n_resourcebase.resource_id = rb.resource_id)
) nl_name,
(
SELECT i18n_resourcebase.NAME
FROM i18n_resourcebase
WHERE language_id=21
AND (i18n_resourcebase.resource_id = rb.resource_id)
) de_name,
(
SELECT i18n_resourcebase.NAME
FROM i18n_resourcebase
WHERE language_id=22
AND (i18n_resourcebase.resource_id = rb.resource_id)
) en_name
FROM resourcebase rb
JOIN resort re ON (rb.resort_id=re.resort_id)
AND re.code='RVRP'
AND rb.TYPE='accommodationtype'
AND rb.archived_from IS NULL
AND re.archived_from IS NULL;
的指导性实验,我认为另一个指导方针是避免重新发明轮子。
答案 1 :(得分:14)
What happens is that because of the <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDummyService" class="com.provinzial.beispielanwendung.batch.wola.DummyServiceImpl" />
</beans>
step, Eigen won't explicitly vectorize .real()
. It will thus call the standard complex::operator* operator, which, unfortunately, is never inlined by gcc. The other versions, on the other hand, uses Eigen's own vectorized product implementation of complexes.
In contrast, ICC does inline complex::operator*, thus making the test2
the fastest for ICC. You can also rewrite test2
as:
test2
to get even better performance on all compilers:
return mat.array().abs2().sum();
The extremely good score of ICC in this case is due to its clever auto-vectorization engine.
Another way to workaround the inlining failure of gcc without modifying 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
is to define your own test2
for operator*
. For instance, add the following at the top of your file:
complex<float>
and then I get:
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));
}
}
Of course, this trick is not always recommended as, in contrast to the glib version, it might lead to overflow or numerical cancellation issues, but this what icpc and the other vectorized versions compute anyway.
答案 2 :(得分:5)
我之前做过的一件事就是大量使用auto
关键字。请记住,大多数Eigen表达式返回特殊表达式数据类型(例如CwiseBinaryOp
),返回Matrix
的赋值可能会强制表达式被评估(这是您所看到的)。使用auto
允许编译器将返回类型推导为它的任何表达式类型,这将尽可能避免评估:
float test1( const MatrixXcf & mat )
{
auto arr = mat.array();
auto conj = arr.conjugate();
auto magc = arr * conj;
auto mag = magc.real();
return mag.sum();
}
这应该基本上更接近你的第二个测试用例。在某些情况下,我在保持可读性的同时获得了良好的性能提升(你不想要拼出表达式模板类型)。当然,您的里程可能会有所不同,因此请仔细测试:)
答案 3 :(得分:0)
我只是想让你注意到你以非最佳的方式进行了分析,所以实际上这个问题可能只是你的分析方法。
由于要考虑缓存局部性等许多问题,您应该以这种方式进行分析:
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
一旦你以正确的方式进行了测试,那么你可以得出结论......
我非常怀疑,由于您一次分析一个操作,最后在第三个测试中使用缓存版本,因为编译器可能会重新排序操作。
此外,您应该尝试使用不同的编译器来查看问题是否是展开模板(优化模板有一个深度限制:很可能您可以使用一个大表达式来点击它。)
此外,如果Eigen支持移动语义,那么没有理由为什么一个版本应该更快,因为并不总能保证表达式可以被优化。
请尝试让我知道,这很有趣。另外一定要确保使用-O3
这样的标志启用优化,没有优化的分析是没有意义的。
为了防止编译器优化所有内容,使用文件的初始输入或cin
,然后重新输入函数内的输入。