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