保证检测临时>命名点

Guaranteed Detection of Temporary->Named Points

本文关键字:gt 检测      更新时间:2023-10-16

假设您编写了一个矩阵类,其中包含一些操作:

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*调用)可以受益于使用动态规划矩阵链乘法算法;非常常见的m0 * m1t具有非常有效的dgemm实现,等等。

因此,将实际计算推迟到需要的时候再执行是值得的。这将上面的代码改为:
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);
...

每个矩阵都有一个指向基类对象的指针,基类对象可以是一个实矩阵,也可以是一个复杂的表达式树。每个操作都试图使用对内部实现最惰性的更改来形成一个矩阵。有些操作别无选择,只能实际求值,压缩表达式树,并将内部实现设置为实际的矩阵。


问题是,在实践中,这在非常常见的情况下会导致巨大的内存开销。假设您从文件中读取一个长而窄的矩阵x = xp x q, p>> q,将xt x存储在一个变量中,并丢弃x。对于惰性求值,内存为pq>> qq。在循环中加载这些,这是一个严重的问题。(当然,客户端代码调用operator()可以在每次加载后强制压缩,但是在没有算法证明的情况下要求压缩是丑陋的,并且容易出错。)

最初,我认为move函数对于自动压缩来说是一个很好的点——它正是临时对象变成命名对象的地方,而命名对象会导致内存消耗增加,所以

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所做的事情,即只是在适当的地方构造它?即使没有,编译器可能会在其他情况下优化掉一些东西吗?

由于"内部指针"方法不能提供延迟求值所需的所有灵活性,因此c++数值库使用的典型解决方案是定义实现延迟求值机制的专用类。c++中的惰性求值问题及其最佳答案展示了这种设计的基础知识和一些示例代码。

虽然我不是专家,但我认为这种体系结构的好例子是数字库Eigen(这里有一些关于其实现的细节)和Blitz++,它们严重依赖于模板(我没有在网上找到说明其内部结构的更新文档,但本文描述了其引擎的一部分,并提供了对"表达式模板"技术的更广泛的概述)。