这个问题是关于如何设计程序,以便进行某些修改.
我有一个类,它包含一些(非平凡的)数据,并有几个成员函数来更改这些数据.
有时我需要计算这些数据的一些属性.但是,每次改变都要从头开始重新计算它.相反,计算这些属性的小更新要快得多.
我有几个这样的属性,我需要能够轻松地添加到我的班级或从我的班级删除(或打开/关闭)进行一些数值实验.该类仅由我自己修改并用于数值模拟(科学代码).
假设我有一个包含数字的类x.但我也需要2^x("属性" x).基本课程是:
class C {
double x;
public:
C() : x(0.0)
{ }
void inc() { x += 1; }
void dec() { x -= 1; }
void set(double x_) { x = x_; }
};
Run Code Online (Sandbox Code Playgroud)
但现在我需要跟踪2^x并在每次x更改时不断更新此值.所以我最终得到了
class expC {
double expx;
public:
expC(const double &x) {
recompute(x);
}
void inc() { expx *= 2; } // fast incremental change
void dec() { expx /= 2; } // fast incremental change
void recompute(const double &x) {
expx = std::pow(2, x); // slow recomputation from scratch
}
};
class C {
double x;
expC prop1; // XX
public:
C() : x(0.0), prop1(x) // XX
{ }
void inc() {
x += 1;
prop1.inc(); // XX
}
void dec() {
x -= 1;
prop1.dec(); // XX
}
void set(double x_) {
x = x_;
prop1.recompute(x); // XX
}
};
Run Code Online (Sandbox Code Playgroud)
XX标志着我需要对课堂做出的改变C.这是很多变化,容易出错.有几个属性变得更加复杂,我甚至相互依赖.
class C {
double x;
expC prop1; // XX
someC prop2; // XX
public:
C() : x(0.0), prop1(x), prop2(x, prop1) // XX
{ }
void inc() {
x += 1;
prop1.inc(); // XX
prop2.inc(); // XX
}
void dec() {
x -= 1;
prop1.dec(); // XX
prop2.dec(); // XX
}
void set(double x_) {
x = x_;
prop1.recompute(x); // XX
prop2.recompute(x, prop1); // XX
}
};
Run Code Online (Sandbox Code Playgroud)
问题:这样的程序有什么好的设计?我确信它可能比上面做的更好.目标是:1)轻松添加/删除此类属性或打开/关闭其计算2)性能至关重要. inc并且dec在紧密的内环中被称为并且相对较少.出于性能原因,它们不能成为虚拟的.
实际上x是一种更复杂的数据结构.可以考虑例如在图表中添加/删除边缘,以及在过程中跟踪其度序列.
更新
@ tobi303要求我展示如何使用这个类.它的方式类似于:
void simulate(C &c) {
for (/* lots of iterations */) {
c.inc();
double p1 = c.prop1.value();
double p2 = c.prop2.value();
if (condition(p1,p2))
c.dec();
}
}
Run Code Online (Sandbox Code Playgroud)
或者说:
它实际上是类似于Metropolis-Hasting算法的蒙特卡罗模拟.
一个具体的例子可能是类C(状态)中的"数据" 是Ising模型的自旋状态(对于熟悉它的人),属性是系统的总能量和总磁化强度.单次旋转翻转后更新速度要快于从头开始重新计算.在实践中我没有Ising模型,我有一些更复杂的东西.我有几个属性,一些快速计算,一些慢(实际上我有一些辅助数据结构,有助于计算属性).我需要尝试不同属性的组合,所以我经常更改我在代码中包含的内容.有时我会实现新属性.当我不需要已经实现的属性时,我需要能够出于性能原因关闭其计算(有些计算速度非常慢).
只是偷懒,在需要时不要计算属性。它将删除大量代码和不必要的计算。
当您确实需要您的属性时,如果它尚未在缓存中,请计算它。因此,每个属性都需要一个布尔值来判断缓存是否是最新的,并且每次x更新时都需要使布尔值无效。
基本上:
class C {
double x;
template <typename Value> struct cachedProp {
bool cache = false;
Value value;
}
cachedProp<expC> prop1;
cachedProp<someC> prop2;
//...
void invalidateCache() {
prop1.cache = false;
prop2.cache = false;
//...
}
public:
expC getProperty1() {
if (!prop1.cache) {
recalculateProp1();
prop1.cache = true;
}
return prop1.value;
}
void inc() {
x += 1;
invalidateCache();
}
};
Run Code Online (Sandbox Code Playgroud)
编辑:一个更懒的解决方案是不存储布尔值cache,而是存储与上次更新相对应的整数,并在 中维护一个计数器C。每次缓存失效时,计数器C都会增加。获取时propX,如果计数器不匹配propX.lastUpdate,则更新 `propX.
这样,使缓存失效只是一项操作,而不必更新所有属性的缓存。