特征3.3.0对3.2.10的性能回归

performance regression with Eigen 3.3.0 vs. 3.2.10?

本文关键字:性能 回归 特征      更新时间:2023-10-16

我们只是在将代码库移植到eigen 3.3的过程中(与所有32个字节对准问题相当一项工作)。但是,与期望相反,在某些地方,表现似乎受到了严重影响(我期待对FMA和AVX的额外支持...)。其中包括特征值分解和矩阵*矩阵。transpose()*矢量产物。我写了两个最小的工作示例以演示。

使用Intel Core i7-4930k CPU(3.40GHz),所有测试在最新的Arch Linux系统上运行,并使用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;
}

测试结果:

  • eigen-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
    
  • eigen-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
    

不确定会导致这一点的原因,但是如果有人可以看到一种维持特征3.3的性能的方法,我想知道它!

2。矩阵*matrix.transpose()*矢量产品:

这个特定的示例需要使用特征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;
}

测试结果:

  • eigen-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
    
  • eigen-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) );

具有巨大的差异,两个特征版本的性能都同样好(实际上3.3.0稍好),并且比未支付的3.2.10情况更快。所以有一个修复程序。尽管如此,奇怪的是3.3.0会为此挣扎。

我不知道这是否是一个错误,但我想如果需要解决的话,这是值得举报的。也许我只是做错了...

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


编辑

正如Ggael指出的那样,如果使用clang++进行编译,则特征3.3中的EVD速度更快,或使用-O3使用g++。这就是问题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

相同的结果,但执行时间大不相同。值得庆幸的是,通过将括号放在正确的位置可以很容易地解决,但是在特征3.3的操作评估中,似乎确实有回归。随着mat.transpose() * VectorXf::Random(SIZE)部分周围的括号,两个特征版本的执行时间均缩短为0.020左右(因此在这种情况下,特征3.2.10也显然受益)。至少这意味着我们可以继续从特征中获得出色的表现!

与此同时,我将接受Ggael的答案,这是我继续前进所需的一切。

对于evd,我无法用clang复制。使用GCC,您需要-O3来避免嵌入问题。然后,使用两个编译器,特征3.3将提供33%的加速。

编辑我先前关于matrix*matrix*vector产品的答案是错误的。这是特征3.3.0中的缺点,将在特征3.3.1中固定。对于记录,我在这里留下我以前的分析,该分析仍然部分有效:

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

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

因此,编译器可以将tmp移出循环。生产代码 不应依靠编译器来执行此类任务,而是 明确移动恒定表达式外部循环。

这是事实,除非在实践中编译器不够聪明,无法将临时性移出循环。