正如 Kerrek SB 所说,这就是所谓的“记忆化”。我将在最后介绍我个人最喜欢的方法(double* array 和更简单的DoubleArray),所以如果你只想看代码,可以跳到那里。但是,有许多方法可以解决这个问题,我想介绍所有方法,包括其他人建议的方法。如果您只想查看代码,请跳至水平线。
第一部分是一些理论和替代方法。这个问题基本上有四个部分:
- 证明函数是幂等的(调用一次函数与调用任意次数是一样的)
- 缓存输入键的结果
- 在给定一组新输入的情况下搜索缓存结果
- 使不再准确/当前的缓存结果失效
第一步对您来说很容易:平均值是幂等的。它没有副作用。
缓存结果是一个有趣的步骤。您显然将为输入创建一些“键”,您可以将其与缓存的“键”进行比较。在 Kerrek SB 的 memoization 示例中,键是所有参数的元组,与 == 的其他键进行比较。在您的系统中,等效的解决方案是将密钥作为整个数组的内容。这意味着每个键比较都是 O(n),这是昂贵的。如果函数的计算成本高于平均函数,那么这个价格可能是可以接受的。然而,在平均的情况下,这个键非常昂贵。
这导致了对好钥匙的开放式搜索。 Dieter Lücking 的答案是键入数组指针。这是 O(1),而且启动速度很快。但是,它还假设一旦计算了数组的平均值,该数组的值就永远不会改变,并且该内存地址永远不会重新用于另一个数组。稍后会在任务的失效部分对此进行解决。
另一个流行的键是 cmets 中的 HotLick (1)。您使用数组的唯一标识符(指针,或者更好的是,永远不会再次使用的唯一整数 idx)作为键。然后,每个数组都有一个“avg 的脏位”,只要值更改,它们就会被设置为 true。缓存首先查找脏位。如果为真,则忽略缓存值,计算新值,缓存新值,然后清除表示缓存值现在有效的脏位。 (这确实是无效,但它很适合这部分答案)
此技术假定对avg 的调用多于对数据的更新。如果数组经常脏,那么 avg 仍然需要不断重新计算,但我们仍然要为每次写入时设置脏位付出代价(减慢它的速度)。
此技术还假设只有一个函数 avg 需要缓存结果。如果您有许多功能,那么让所有脏位保持最新会变得很昂贵。解决方案是一个“纪元”计数器。你有一个整数,而不是一个脏位,它从 0 开始。每次写入都会增加它。当你缓存一个结果时,你不仅缓存了数组的标识,还缓存了它的纪元。当您检查是否有缓存值时,您还要检查时期是否发生了变化。如果它确实发生了变化,您就无法证明您的旧结果是最新的,并且必须将它们丢弃。
存储结果是一项有趣的任务。通过将数十万个旧结果记住到avg 来编写一个使用大量内存的存储算法非常容易。一般来说,需要有一种方法让缓存代码知道一个数组已被破坏,或者一种方法可以慢慢移除旧的未使用缓存结果。在前一种情况下,双数组的释放器需要让缓存代码知道该数组正在被释放。在后一种情况下,通常将缓存限制为 10 或 100 个条目,并驱逐旧的缓存结果。
最后一点是缓存失效。我之前谈到了肮脏的部分。对此的一般模式是,如果缓存中的值没有更改,则必须将缓存中的值标记为无效,但数组中的值确实更改了。如果键是数组的副本,这显然永远不会发生,但当键是标识整数或指针时,就会发生这种情况。
一般来说,失效是向调用者添加要求的一种方式:如果您想将 avg 与缓存一起使用,则需要执行以下额外工作来帮助缓存代码。
最近我用这种缓存失效方案实现了一个系统。它非常简单,源于一种理念:调用avg 的代码比avg 本身更能确定数组是否发生了变化。
-
avg 的等效项有两个版本:double avg(double* array, int n) 和 double avg(double* array, int n, CacheValidityObject& validity)。
- 从未缓存调用
avg 的 2 参数版本,因为它不能保证数组没有更改。
- 调用
avg 激活缓存的3 参数版本。调用方保证,如果将相同的 CacheValidityObject 传递给 avg 而不将其标记为脏,则数组必须相同。
将责任推给来电者让普通人变得微不足道。 CacheValidityObject 是一个非常简单的类来保存结果
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
}
优势
- 几乎是最快的缓存(在几个操作码内)
- 实现起来很简单
- 不泄漏内存,仅在调用者认为可能需要再次使用缓存值时才保存缓存值
缺点
如果您将double* array 包装在一个类中,您可以最大限度地减少缺点。为每个算法分配一个索引(可以在运行时完成)让DoubleArray 类维护一个缓存值的映射。对DoubleArray 的每次修改都会使缓存的结果无效。这是最易于使用的版本,但不适用于裸数组...您需要一个类来帮助您
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