缓存重复输入数据的表达式求值

Caching of expression evaluations for repeated input data

本文关键字:表达式 数据 输入 缓存      更新时间:2023-10-16

我有一对从

派生的简单的"表达式"类
namespace Expression {
class Virtual {
  public:
  Virtual();
  ~Virtual();
  virtual
  double eval(double x, double y, double z) const = 0; 
}
}
例如

class MyFun: public Virtual {
  public:
  MyFun();
  ~MyFun();
  virtual
  double eval(double x, double y, double z) const
  {
      return sin(x*x) + exp(z)*y + 3.0; // some expensive expression
  } 
}

这些表达式在许多但非常具体的xyz中是eval ed,即网格的节点

class MyMesh {
  MyMesh();
  ~MyMesh();
  virtual
  std::vector<std::vector<double> > getNodes() const
  {
      return allMyNodes_;
  } 
}

调用者将实例化一个网格,实例化一个Expression,然后继续进行一些数值过程,其中Expression在网格节点中可能被评估多次。

MyMesh myMesh();
MyFun myExpr();
Result result = numericalProcedure(myMesh, myExpr);
// [...]

由于eval是昂贵的,我想如何加快这一点。我想到的是缓存。

一个想法是将所有eval结果存储在具有一些索引的矢量中的所有网格节点中,我想知道如何最好地实现这一点。我不想在 Expressions中粘贴任何网格数据(以保持缓存),也不想为被调用者制作更复杂的接口。

是否存在与此用例匹配的缓存设计模式?将缓存逻辑与其余代码隔离开来的方法是什么?

可以在函函数中存储从实参数组到结果的映射:

class MyFun {
public:
    virtual
    double eval(double x, double y, double z) const
    {
        std::array<double,3> arr {x,y,z};
        //is the answer cached?
        if (cache_.count(arr))
        {
            return cache_[arr];
        }
        double ret = sin(x*x) + exp(z)*y + 3.0;
        //cache the result
        cache_[arr] = ret;
        return ret;
    } 
private:
    //need mutable so it can be modified by a const function
    mutable std::map<std::array<double,3>, double> cache_;
};

您甚至可以在基类中进行缓存,然后将求值转发给虚函数:

class BaseFun {
public:
    double eval(double x, double y, double z) const
    {
        std::array<double,3> arr {x,y,z};
        if (cache_.count(arr))
        {
            return cache_[arr];
        }
        double ret = doEval(x,y,z);
        cache_[arr] = ret;
        return ret;
    }   
protected:
    virtual double doEval (double x, double y, double z) const = 0;
private:
    mutable std::map<std::array<double,3>, double> cache_;
};
class MyFun : public BaseFun {
private:
    virtual double doEval (double x, double y, double z) const override
    {
        return sin(x*x) + exp(z)*y + 3.0;
    }
};

将缓存逻辑与其他代码隔离的方法是什么?

可以将缓存分离到具有相同接口的另一个类中,该类可以缓存任何表达式的结果:

class Cache : public Virtual {
public:
    explicit Cache(Virtual const & target) : target(target) {}
    double eval(double x, double y, double z) const override {
       std::array<double,3> key {x,y,z};
       auto cached = cache.find(key);
       if (cached == cache.end()) {
           double result = target(x,y,z);
           cache[key] = result;
           return result;
       } else {
           return cached->second;
       }
    }
private:
    Virtual const & target;
    std::map<std::array<double,3>, double> cache;
};

可以这样使用

Result result = numericalProcedure(myMesh, Cache(myExpr));

将临时缓存封装在现有表达式的周围。

(如果你想使用一个更永久的缓存,那么它应该有一个失效策略来阻止它变得太大。我的简单示例仅在销毁时释放内存,因此如果它从未销毁,则可能成为内存泄漏。

是否存在与此用例匹配的缓存设计模式?

如果你想给它命名,那就是代理模式的一个例子。

我将实现一个新的Expression::Virtual子类,它将只做缓存,同时保留另一个表达式的实例,将实际计算委托给它:

class CachedExpression: public Expression::Virtual {
    private:
       // for simplicity I assume a separate Point class
       mutable std::map<Point, double> cache_; 
       const Expression::Virtual* expr_; // or better auto_ptr or friends
    public: 
       explicit CachedExpression(const Expression::Virtual* expr): cache_(), expr_(expr) {}
       virtual double eval(const Point& point) const {
           if (cache_.find(point) == cache_.end())
               cache_[point] = expr_.eval(point);
           return cache_[point];
       }
};
...
MyMesh myMesh;
MyFun myExpr;
CachedExpression myCached(&myExpr);
// or even CachedExpression myCached(new myFun());
Result result = numericalProcedure(myMesh, myCached);

通过这种方式,您总是可以通过仅使用myExpr而不是myCached来关闭缓存,或者在不同的CachedExpression类中实现不同的缓存策略(例如仅保留N最后查询以节省内存),并使用您需要的任何缓存。