C++ 避免在数据未更改时重新计算

C++ 避免在数据未更改时重新计算,c++,arrays,dynamic,C++,Arrays,Dynamic,假设您有一个相当大的double数组和一个计算平均值的简单函数avg(double*,size\u t)(仅举一个简单的例子:数组和函数可以是任何数据结构和算法)。我希望,如果第二次调用该函数,同时数组没有改变,则返回值直接来自前一次调用,而不经过未更改的数据 要保持前一个值看起来很简单,我只需要在函数中使用一个静态变量,对吗?但是如何检测阵列中的变化呢?我是否需要编写一个接口来访问设置该函数要读取的标志的数组?可以做一些更智能、更便携的事情吗?#包括 #include <limits&g

假设您有一个相当大的double数组和一个计算平均值的简单函数
avg(double*,size\u t)
(仅举一个简单的例子:数组和函数可以是任何数据结构和算法)。我希望,如果第二次调用该函数,同时数组没有改变,则返回值直接来自前一次调用,而不经过未更改的数据

要保持前一个值看起来很简单,我只需要在函数中使用一个静态变量,对吗?但是如何检测阵列中的变化呢?我是否需要编写一个接口来访问设置该函数要读取的标志的数组?可以做一些更智能、更便携的事情吗?

#包括
#include <limits>
#include <map>

// Note: You have to manage cached results - release it with avg(p, 0)! 
double avg(double* p, std::size_t n) {
    typedef std::map<double*, double> map;
    static map results;
    map::iterator pos = results.find(p);
    if(n) {
        // Calculate or get a cached value
        if(pos == results.end()) {
            pos = results.insert(map::value_type(p, 0.5)).first; // calculate it
        }
        return pos->second;
    }
    // Erase a cached value
    results.erase(pos);
    return std::numeric_limits<double>::quiet_NaN();
}
#包括 //注意:您必须管理缓存结果-使用平均值(p,0)发布它! 双平均值(双*p,标准::尺寸){ typedef std::map; 静态地图结果; 迭代器pos=results.find(p); 如果(n){ //计算或获取缓存的值 if(pos==results.end()){ pos=results.insert(map::value_type(p,0.5))。首先;//计算它 } 返回位置->秒; } //删除缓存的值 结果:擦除(pos); 返回std::numeric_limits::quiet_NaN(); }
正如Kerrek SB所精辟地指出的,这被称为“记忆”。最后我将介绍我个人最喜欢的方法(使用
双*数组和更简单的
双*数组),因此如果您只想看代码,可以跳到那里。然而,有很多方法可以解决这个问题,我想把它们都包括在内,包括其他人建议的方法。如果您只想查看代码,请跳到水平规则

第一部分是一些理论和替代方法。这个问题基本上有四个部分:

  • 证明函数是幂等的(调用一次函数等于调用任意次数)
  • 缓存键入到输入的结果
  • 在给定一组新输入的情况下搜索缓存结果
  • 使不再准确/当前的缓存结果无效
第一步很简单:平均值是幂等的。它没有副作用

缓存结果是一个有趣的步骤。显然,您将为输入创建一些“键”,以便与缓存的“键”进行比较。在Kerrek SB的示例中,键是所有参数的元组,与其他具有
=
的键进行比较。在您的系统中,等效的解决方案是将密钥设置为整个数组的内容。这意味着每个键比较都是O(n),这是昂贵的。如果该函数的计算成本高于平均函数,则该价格可能是可以接受的。然而,在平均的情况下,这个关键是非常昂贵的

这导致了一种对好钥匙的开放式搜索。Dieter Lückk的答案是为数组指针设置键。这是O(1),引导速度非常快。但是,它还假设,一旦计算了一个数组的平均值,该数组的值永远不会更改,并且内存地址永远不会用于另一个数组。这方面的解决方案稍后将在任务的失效部分提供

另一个流行的关键字是HotLick在评论中的(1)。您使用数组的唯一标识符(指针,或者更好的是,一个永远不会使用的唯一整数idx)作为密钥。然后,每个数组都有一个“平均值
avg
”的脏位,每当值更改时,它们都会被设置为true。缓存首先查找脏位。如果为true,则忽略缓存值,计算新值,缓存新值,然后清除脏位,指示缓存值现在有效。(这确实是无效的,但它很适合回答的这一部分)

此技术假定对
avg
的调用多于对数据的更新。如果数组不断地变脏,那么avg仍然需要继续重新计算,但我们仍然要为每次写入设置脏位付出代价(降低速度)

该技术还假设只有一个函数,
avg
,需要缓存结果。如果你有很多函数,那么让所有脏的部分都保持最新就开始变得昂贵。解决方案是一个“历元”计数器。不是脏位,而是一个从0开始的整数。每写一次,它就会递增。缓存结果时,不仅要缓存数组的标识,还要缓存其历元。当您检查是否有缓存的值时,也会检查历元是否已更改。如果它真的改变了,你就不能证明你以前的结果是最新的,你必须把它们扔掉

存储结果是一项有趣的任务。编写一个存储算法非常容易,它通过将数十万个旧结果存储到
avg
中来消耗大量内存。一般来说,需要有一种方法让缓存代码知道数组已被破坏,或者有一种方法慢慢删除旧的未使用缓存结果。在前一种情况下,双数组的deallocator需要让缓存代码知道该数组正在被解除分配。在后一种情况下,通常将缓存限制为10个或100个条目,并具有逐出旧缓存结果

最后一点是缓存的失效。我早些时候谈到了肮脏的一点。通常情况下,如果缓存中存储的键没有更改,但数组中的值确实更改,则缓存中的值必须标记为无效。如果键是数组的副本,则显然永远不会发生这种情况,但如果键是标识整数或指针,则可能发生这种情况

一般来说,失效是一种向调用者添加需求的方法:如果您想将
avg
与缓存一起使用,那么需要做的额外工作如下
class CacheValidityObject
{
    public:
        CacheValidityObject(); // creates a new dirty CacheValidityObject

        void invalidate(); // marks this object as dirty


        // this function is used only by the `avg` algorithm.  "friend" may
        // be used here, but this example makes it public
        boost::shared_ptr<void>& getData();
    private:
        boost::shared_ptr<void>  mData;
};

inline void CacheValidityObject::invalidate()
{
    mData.reset(); // blow away any cached data
}

double avg(double* array, int n); // defined as usual

double avg(double* array, int n, CacheValidityObject& validity)
{
    // this function assumes validity.mData is null or a shared_ptr to a double
    boost::shared_ptr<void>& data = validity.getData();
    if (data) {
        // The cached result, stored on the validity object, is still valid
        return *static_pointer_cast<double>(data);
    } else {
        // There was no cached result, or it was invalidated
        double result = avg(array, n);
        data = make_shared<double>(result); // cache the result
        return result;
    }
}

// usage
{
    double data[100];
    fillWithRandom(data, 100);

    CacheValidityObject dataCacheValidity;
    double a = avg(data, 100, dataCacheValidity); // caches the aveerage
    double b = avg(data, 100, dataCacheValidity); // cache hit... uses cached result

    data[0] = 0;
    dataCacheValidity.invalidate();
    double c = avg(data, 100, dataCacheValidity); // dirty.. caches new result
    double d = avg(data, 100, dataCacheValidity); // cache hit.. uses cached result

    // CacheValidityObject::~CacheValidityObject() will destroy the shared_ptr,
    // freeing the memory used to cache the result
}
class DoubleArray
{
    public:
        // all of the getters and setters and constructors.
        // Special note: all setters MUST call invalidate()

        CacheValidityObject getCache(int inIdx)
        {
            return mCaches[inIdx];
        }

        void setCache(int inIdx, const CacheValidityObject& inObj)
        {
            mCaches[inIdx] = inObj;
        }

    private:
        void invalidate()
        {
            mCaches.clear();
        }

        std::map<int, CacheValidityObject> mCaches;
        double*                            mArray;
        int                                mSize;
};

inline int getNextAlgorithmIdx()
{
    static int nextIdx = 1;
    return nextIdx++;
}


static const int avgAlgorithmIdx = getNextAlgorithmIdx();
double avg(DoubleArray& inArray)
{
    CacheValidityObject valid = inArray.getCache(avgAlgorithmIdx);
    // use the 3 argument avg in the previous example
    double result = avg(inArray.getArray(), inArray.getSize(), valid);
    inArray.setCache(avgAlgorithmIdx, valid);
    return result;
}

// usage
DoubleArray  array(100);
fillRandom(array);
double a = avg(array); // calculates, and caches
double b = avg(array); // cache hit
array.set(0, 5); // invalidates caches
double c = avg(array); // calculates, and caches
double d = avg(array); // cache hit