保证检测临时->命名点
Guaranteed Detection of Temporary->Named Points
假设你用一些操作写了一个矩阵class:
class matrix
{
public:
double operator()(size_t i, size_t j) const;
...
};
matrix operator*(const matrix &lhs, const matrix &rhs);
...
延迟某些矩阵表达式的计算是有意义的:m0 * m1 * m2 * m3 * m4(这是一系列四个 operator*
调用)可以受益于使用 dynamic-programming matrix chain multiplication algorithm; the very common m0 * m1t has a very efficient dgemm
implementation,等等。
因此,将实际计算推迟到需要的时候是值得的。这会将上面的内容更改为:
class matrix
{
private:
/*
* Pointer to an abstract base class - either an actual matrix,
* or an expression tree. */
std::shared_ptr<matrix_imp> m_imp;
public:
// Forces compaction -
double operator()(size_t i, size_t j) const;
...
};
/* Lazy; creates a matrix with an expression tree using the
* internals of lhs and rhs. */
matrix operator*(const matrix &lhs, const matrix &rhs);
...
每个矩阵都包含一个指向基础 class 对象的指针,该对象的范围可以从实际矩阵到复杂的表达式树。每个操作都尝试使用对内部实现的最懒惰的更改来形成一个矩阵。有些操作别无选择,只能实际评估事物,压缩表达式树,并将内部实现设置为实际矩阵。
问题是,在实践中,这在非常常见的情况下会导致巨大的内存开销。假设您从文件中读取了一个狭长矩阵 x = xp X q, p >> q, store xtx一个变量,丢弃x。使用惰性求值,内存为 pq >> qq。循环加载这些,这是一个严重的问题。 (当然,客户端代码调用 operator()
可以在每次加载后强制执行压缩,但是在没有算法证明的情况下要求这样做是丑陋且容易出错的。)
最初,我认为 move ctor 是自动压缩的好点 - 它正是临时成为命名对象的地方,它是导致内存消耗增加的命名对象,所以
matrix(matrix &&other); // <- Force compaction only here
似乎可以解决所有问题,例如
auto res = // <- temp becoming named
a * // temp
b * // temp
c + // temp
2 * // temp
d;
但是可以指望吗?例如,考虑
matrix load_xtx(const string &f_name)
{
matrix x = ...
return x.t() * x;
}
auto xtx = load_xtx("foo.hdf5"); // (*)
编译器是否被禁止在 (*)
中执行类似于它在 NRVO 中执行的操作,即仅在适当的位置构造它?即使没有,编译器是否可以优化其他情况下的东西?
由于"internal pointer" 方法无法提供延迟求值所需的全部灵活性,C++ 数值库使用的典型解决方案是定义专门的类 实现惰性求值机制。旧的 SO 问题 Lazy evaluation in C++ 及其最佳答案显示了此类设计的基础知识和一些示例代码。
虽然我不是专家,但我认为这种架构的很好的例子是数字库 Eigen(here some details about its implementation) and Blitz++, which heavily relies on templates (I did not find on the web updated documentation illustrating its internals, but this article 描述了它的引擎的某些部分,还提供了 "expression template" 技术的更广泛的概述).
假设你用一些操作写了一个矩阵class:
class matrix
{
public:
double operator()(size_t i, size_t j) const;
...
};
matrix operator*(const matrix &lhs, const matrix &rhs);
...
延迟某些矩阵表达式的计算是有意义的:m0 * m1 * m2 * m3 * m4(这是一系列四个 operator*
调用)可以受益于使用 dynamic-programming matrix chain multiplication algorithm; the very common m0 * m1t has a very efficient dgemm
implementation,等等。
因此,将实际计算推迟到需要的时候是值得的。这会将上面的内容更改为:
class matrix
{
private:
/*
* Pointer to an abstract base class - either an actual matrix,
* or an expression tree. */
std::shared_ptr<matrix_imp> m_imp;
public:
// Forces compaction -
double operator()(size_t i, size_t j) const;
...
};
/* Lazy; creates a matrix with an expression tree using the
* internals of lhs and rhs. */
matrix operator*(const matrix &lhs, const matrix &rhs);
...
每个矩阵都包含一个指向基础 class 对象的指针,该对象的范围可以从实际矩阵到复杂的表达式树。每个操作都尝试使用对内部实现的最懒惰的更改来形成一个矩阵。有些操作别无选择,只能实际评估事物,压缩表达式树,并将内部实现设置为实际矩阵。
问题是,在实践中,这在非常常见的情况下会导致巨大的内存开销。假设您从文件中读取了一个狭长矩阵 x = xp X q, p >> q, store xtx一个变量,丢弃x。使用惰性求值,内存为 pq >> qq。循环加载这些,这是一个严重的问题。 (当然,客户端代码调用 operator()
可以在每次加载后强制执行压缩,但是在没有算法证明的情况下要求这样做是丑陋且容易出错的。)
最初,我认为 move ctor 是自动压缩的好点 - 它正是临时成为命名对象的地方,它是导致内存消耗增加的命名对象,所以
matrix(matrix &&other); // <- Force compaction only here
似乎可以解决所有问题,例如
auto res = // <- temp becoming named
a * // temp
b * // temp
c + // temp
2 * // temp
d;
但是可以指望吗?例如,考虑
matrix load_xtx(const string &f_name)
{
matrix x = ...
return x.t() * x;
}
auto xtx = load_xtx("foo.hdf5"); // (*)
编译器是否被禁止在 (*)
中执行类似于它在 NRVO 中执行的操作,即仅在适当的位置构造它?即使没有,编译器是否可以优化其他情况下的东西?
由于"internal pointer" 方法无法提供延迟求值所需的全部灵活性,C++ 数值库使用的典型解决方案是定义专门的类 实现惰性求值机制。旧的 SO 问题 Lazy evaluation in C++ 及其最佳答案显示了此类设计的基础知识和一些示例代码。
虽然我不是专家,但我认为这种架构的很好的例子是数字库 Eigen(here some details about its implementation) and Blitz++, which heavily relies on templates (I did not find on the web updated documentation illustrating its internals, but this article 描述了它的引擎的某些部分,还提供了 "expression template" 技术的更广泛的概述).