性能回归与特征3.3.0与3.2.10?

时间:2016-11-25 12:44:08

标签: c++ eigen eigen3

我们正处于porting our codebase到Eigen 3.3的过程中(相当于所有32字节对齐问题的承诺)。然而,有一些地方的表现似乎受到严重影响,与预期相反(考虑到对FMA和AVX的额外支持,我期待一些加速......)。这些包括特征值分解和矩阵* matrix.transpose()*矢量积。我写了两个最小的工作示例来演示。

所有测试均在最新的Arch Linux系统上运行,使用Intel Core i7-4930K CPU(3.40GHz),并使用g ++版本6.2.1进行编译。

1。特征值分解:

一个简单的自伴随特征值分解与本征3.3.0一样长,与3.2.10一样长。

档案test_eigen_EVD.cpp

#define EIGEN_DONT_PARALLELIZE
#include <Eigen/Dense>
#include <Eigen/Eigenvalues>

#define SIZE 200
using namespace Eigen;

int main (int argc, char* argv[])
{
  MatrixXf mat = MatrixXf::Random(SIZE,SIZE);
  SelfAdjointEigenSolver<MatrixXf> eig;

  for (int n = 0; n < 1000; ++n)
    eig.compute (mat);

  return 0;
}

测试结果:

  • 固有3.2.10:

    g++ -march=native -O2 -DNDEBUG -isystem eigen-3.2.10 test_eigen_EVD.cpp -o test_eigen_EVD && time ./test_eigen_EVD
    
    real    0m5.136s
    user    0m5.133s
    sys     0m0.000s
    
  • 固有3.3.0:

    g++ -march=native -O2 -DNDEBUG -isystem eigen-3.3.0 test_eigen_EVD.cpp -o test_eigen_EVD && time ./test_eigen_EVD
    
    real    0m11.008s
    user    0m11.007s
    sys     0m0.000s
    

不确定是什么导致这种情况,但是如果有人能看到使用Eigen 3.3保持性能的方法,我想知道它!

2。 matrix * matrix.transpose()* vector product:

这个特殊的例子与Eigen 3.3.0相比要长200倍......

档案test_eigen_products.cpp

#define EIGEN_DONT_PARALLELIZE
#include <Eigen/Dense>

#define SIZE 200
using namespace Eigen;

int main (int argc, char* argv[])
{
  MatrixXf mat = MatrixXf::Random(SIZE,SIZE);
  VectorXf vec = VectorXf::Random(SIZE);

  for (int n = 0; n < 50; ++n)
    vec = mat * mat.transpose() * VectorXf::Random(SIZE);

  return vec[0] == 0.0;
}

测试结果:

  • 固有3.2.10:

    g++ -march=native -O2 -DNDEBUG -isystem eigen-3.2.10 test_eigen_products.cpp -o test_eigen_products && time ./test_eigen_products
    
    real    0m0.040s
    user    0m0.037s
    sys     0m0.000s
    
  • 固有3.3.0:

    g++ -march=native -O2 -DNDEBUG -isystem eigen-3.3.0 test_eigen_products.cpp -o test_eigen_products && time ./test_eigen_products
    
    real    0m8.112s
    user    0m7.700s
    sys     0m0.410s
    

在循环中的行中添加括号,如下所示:

    vec = mat * ( mat.transpose() * VectorXf::Random(SIZE) );

产生了巨大的差异,两个Eigen版本的表现同样出色(实际上3.3.0稍好一些),并且比没有括号的3.2.10情况更快。所以有一个修复。不过,奇怪的是,3.3.0会因此而如此挣扎。

我不知道这是否是一个错误,但我想这是值得报道,以防这是需要修复的事情。或许我只是做错了......

任何想法都赞赏。 干杯, 唐纳德。

修改

作为pointed out by ggael,如果使用clang++-O3使用g++进行编译,则Eigen 3.3中的EVD会更快。所以问题1已修复。

问题2并不是真正的问题,因为我可以使用括号来强制执行最有效的操作顺序。但仅仅是为了完整性:在评估这些操作时似乎存在某种缺陷。 Eigen是一个令人难以置信的软件,我认为这可能值得修复。这是MWE的修改版本,只是为了表明它不太可能与第一个被淘汰的临时产品有关(至少据我所知):

#define EIGEN_DONT_PARALLELIZE
#include <Eigen/Dense>
#include <iostream>

#define SIZE 200
using namespace Eigen;

int main (int argc, char* argv[])
{
  VectorXf vec (SIZE), vecsum (SIZE);
  MatrixXf mat (SIZE,SIZE);

  for (int n = 0; n < 50; ++n) {
    mat = MatrixXf::Random(SIZE,SIZE);
    vec = VectorXf::Random(SIZE);
    vecsum += mat * mat.transpose() * VectorXf::Random(SIZE);
  }

  std::cout << vecsum.norm() << std::endl;
  return 0;
}

在这个例子中,操作数都在循环中初始化,结果在vecsum中累积,因此编译器无法预先计算任何内容,或者优化掉不必要的计算。这显示了完全相同的行为(这次使用clang++ -O3(版本3.9.0)进行测试:

$ clang++ -march=native -O3 -DNDEBUG -isystem eigen-3.2.10 test_eigen_products.cpp -o test_eigen_products && time ./test_eigen_products
5467.82

real    0m0.060s
user    0m0.057s
sys     0m0.000s

$ clang++ -march=native -O3 -DNDEBUG -isystem eigen-3.3.0 test_eigen_products.cpp -o test_eigen_products && time ./test_eigen_products
5467.82

real    0m4.225s
user    0m3.873s
sys     0m0.350s

同样的结果,但执行时间差异很大。值得庆幸的是,通过将括号放在正确的位置可以很容易地解决这个问题,但在Eigen 3.3的运算评估中似乎确实存在回归。使用mat.transpose() * VectorXf::Random(SIZE)部分周围的括号,两个本征版本的执行时间减少到大约0.020秒(因此在这种情况下,Eigen 3.2.10显然也有好处)。至少这意味着我们可以继续从Eigen中获得出色的表现!

与此同时,我会接受ggael的回答,这是我向前迈进所需要知道的全部内容。

1 个答案:

答案 0 :(得分:2)

对于EVD,我不能用clang重现。使用gcc,您需要-O3以避免内联问题。然后,使用这两个编译器,Eigen 3.3将提供33%的加速。

编辑我之前关于matrix*matrix*vector产品的回答是错误的。这是Eigen 3.3.0的一个缺点,将在Eigen 3.3.1中修复。为了记录,我留在这里我之前的分析仍然部分有效:

  

正如您所注意到的,您应该添加括号来执行两个   matrix*vector个产品,而非大型matrix*matrix产品。   然后通过3.2中的事实很容易解释速度差异,   立即评估嵌套的matrix*matrix产品(at   嵌套时间),而在3.3中,它是在评估时评估的   在operator=。这意味着在3.2中,循环等同于:

for (int n = 0; n < 50; ++n) {
  MatrixXf tmp = mat * mat.transpose();
  vec = tmp * VectorXf::Random(SIZE);
}
     

因此编译器可以将tmp移出循环。生产代码   不应该依赖编译器来完成这种任务   显式地将循环中的常量表达式移动。

这是事实,除了在实践中编译器不够智能以将临时移出循环。