Eigen C ++:稀疏矩阵操作的性能

时间:2016-08-03 10:16:20

标签: c++ matrix eigen

任何人都可以解释特征稀疏矩阵的以下行为吗?我一直在调查aliasinglazy evaluation,但我似乎无法改善这个问题。技术规格:我在Ubuntu 16.10上使用最新的Eigen稳定版本,带有g ++编译器,没有优化标记。

假设我按以下方式定义一个简单的身份:

SparseMatrix<double> spIdent(N,N);
spIdent.reserve(N);
spIdent.setIdentity();

然后用它执行这些操作

spIdent-spIdent;
spIdent*spIdent;
spIdent - spIdent*spIdent;

并测量所有三个的计算时间。我得到的是这个

0 Computation time: 2.6e-05
1 Computation time: 2e-06 
2 Computation time: 1.10706

意味着任一操作都很快,但组合速度超慢。 noalias()方法仅为密集矩阵定义,而在我的密集示例中,它没有太大的区别。任何启示?

MCVE:

#include <iostream>
#include <ctime>
#include "../Eigen/Sparse"

using namespace std;
using namespace Eigen;

int main() {

unsigned int N=2000000;

SparseMatrix<double> spIdent(N,N);
spIdent.reserve(N);
spIdent.setIdentity();

clock_t start=clock();
spIdent*spIdent;
cout << "0 Computation time: " << float(clock() - start)/1e6 << '\n';

start=clock();
spIdent-spIdent;
cout << "1 Computation time: " << float(clock() - start)/1e6 << '\n';

start=clock();
spIdent - (spIdent*spIdent);
cout << "2 Computation time: " << float(clock() - start)/1e6 << '\n';

return 0;

}

3 个答案:

答案 0 :(得分:4)

并没有像懒惰的评估那样优化它,非常懒惰。看看产品。被调用的代码(至少在本机上包含的任何Eigen版本中):

template<typename Derived>
template<typename OtherDerived>
inline const typename SparseSparseProductReturnType<Derived,OtherDerived>::Type
SparseMatrixBase<Derived>::operator*(const SparseMatrixBase<OtherDerived> &other) const
{
  return typename SparseSparseProductReturnType<Derived,OtherDerived>::Type(derived(), other.derived());
}

返回产品的表达式(即懒惰)。此表达式没有任何操作,因此成本为零。差异也是如此。现在,在执行a-a*a时,a*a是一个表达式。然后它符合operator-。这看到了右侧的表达。然后将该表达式评估为临时(即成本时间),以便在operator-中使用它。为什么要评估一个临时的?阅读this了解其逻辑(搜索“第二种情况”)。

operator-CwiseBinaryOp,产品表达为右侧。 CwiseBinaryOp做的第一件事是将右侧分配给成员:

EIGEN_STRONG_INLINE CwiseBinaryOp(const Lhs& aLhs, const Rhs& aRhs, const BinaryOp& func = BinaryOp())
      : m_lhs(aLhs), m_rhs(aRhs), m_functor(func)

m_rhs(aRhs))反过来调用SparseMatrix构造函数:

/** Constructs a sparse matrix from the sparse expression \a other */
template<typename OtherDerived>
inline SparseMatrix(const SparseMatrixBase<OtherDerived>& other)
  : m_outerSize(0), m_innerSize(0), m_outerIndex(0), m_innerNonZeros(0)
{
  ...
  *this = other.derived();
}

反过来调用operator=(如果我错了,有人会纠正我)总是会触发评估,在这种情况下是暂时的。

答案 1 :(得分:3)

好吧,正如人们提到的那样,在前两个语句中,代码完全被优化掉(我已经使用当前版本的g ++和-O3集进行了测试)。反汇编显示了第二个语句:

  400e78:   e8 03 fe ff ff          callq  400c80 <clock@plt>   # timing begins
  400e7d:   48 89 c5                mov    %rax,%rbp
  400e80:   e8 fb fd ff ff          callq  400c80 <clock@plt>   # timing ends

Fop第三部分实际上发生了一些事情,称为Eigen库代码:

  400ede:   e8 9d fd ff ff          callq  400c80 <clock@plt>   # timing begins
  400ee3:   48 89 c5                mov    %rax,%rbp
  400ee6:   8b 44 24 58             mov    0x58(%rsp),%eax
  400eea:   39 44 24 54             cmp    %eax,0x54(%rsp)
  400eee:   c6 44 24 20 00          movb   $0x0,0x20(%rsp)
  400ef3:   48 89 5c 24 28          mov    %rbx,0x28(%rsp)
  400ef8:   48 89 5c 24 30          mov    %rbx,0x30(%rsp)
  400efd:   48 c7 44 24 38 00 00    movq   $0x0,0x38(%rsp)
  400f04:   00 00 
  400f06:   c6 44 24 40 01          movb   $0x1,0x40(%rsp)
  400f0b:   0f 85 99 00 00 00       jne    400faa <main+0x22a>
  400f11:   48 8d 4c 24 1f          lea    0x1f(%rsp),%rcx
  400f16:   48 8d 54 24 20          lea    0x20(%rsp),%rdx
  400f1b:   48 8d bc 24 90 00 00    lea    0x90(%rsp),%rdi
  400f22:   00 
  400f23:   48 89 de                mov    %rbx,%rsi
  400f26:   e8 25 1a 00 00          callq  402950 <_ZN5Eigen13CwiseBinaryOpINS_8internal20scalar_difference_opIdEEKNS_12SparseMatrixIdLi0EiEEKNS_19SparseSparseProductIRS6_S8_EEEC1ES8_RSA_RKS3_>
  400f2b:   48 8d bc 24 a0 00 00    lea    0xa0(%rsp),%rdi
  400f32:   00 
  400f33:   e8 18 02 00 00          callq  401150 <_ZN5Eigen12SparseMatrixIdLi0EiED1Ev>
  400f38:   e8 43 fd ff ff          callq  400c80 <clock@plt>   # timing ends

我想在这种情况下,编译器无法确定计算结果是否未使用,而不是前两种情况。

如果您查看documentation,那么您可以看到稀疏矩阵上的+之类的简单操作不返回矩阵,而是返回表示结果的CwiseUnaryOp。我想如果你不在某个地方使用这个类,那么结果矩阵永远不会被构造。

答案 2 :(得分:2)

我认为正如@ hfhc2所提到的,代码中的前两个语句完全由编译器完全优化(因为其余的结果不需要)。在第三个语句中,最有可能产生一个辅助中间变量来存储spIdent*spIdent的临时结果。 要清楚地看到这一点,请考虑以下示例,其中包括显式复制分配:

#include <iostream>
#include <ctime>
#include <Eigen/Sparse>

using namespace std;
using namespace Eigen;

int main () {

   const unsigned int N = 2000000;

   SparseMatrix<double> spIdent(N,N);
   SparseMatrix<double> a(N,N), b(N,N), c(N,N);

   spIdent.reserve(N);
   spIdent.setIdentity();

   clock_t start = clock();
   a = spIdent*spIdent;
   cout << "0 Computation time: " << float(clock() - start)/1e6 << endl;

   start = clock();
   b = spIdent-spIdent;
   cout << "1 Computation time: " << float(clock() - start)/1e6 << endl;

   start = clock();
   c = a - b;
   cout << "2 Computation time: " << float(clock() - start)/1e6 << endl;

   return 0;

} 

测量的时间(没有编译器优化)是[对于openSUSE 12.2(x86_64),g ++ 4.7.1,Intel 2core 2GHz CPU]:

0 Computation time: 1.58737
1 Computation time: 0.417798
2 Computation time: 0.428174

这似乎很合理。