任何人都可以解释特征稀疏矩阵的以下行为吗?我一直在调查aliasing和lazy 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;
}
答案 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
这似乎很合理。