This question is about how to design a program so it will be easy to make certain modifications.
I have a class which holds some (non-trivial) data and has several member functions that change this data.
Sometimes I need to compute some property of this data. But it is slow to recompute it from scratch on every change. It is much faster to compute a small update to these properties instead.
I have several such properties which I need to be able to easily add or remove to/from my class (or turn on/off) to carry out some numerical experiments. The class is only modified by myself and is used for numerical simulations (scientific code).
Let's say I have a class that holds a number x
. But I also need 2^x
(a "property" of x
). The basic class is:
class C {
double x;
public:
C() : x(0.0)
{ }
void inc() { x += 1; }
void dec() { x -= 1; }
void set(double x_) { x = x_; }
};
But now I need to keep track of 2^x
and to keep updating this value every time x
changes. So I end up with
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
}
};
XX
marks changes I needed to make to the class C
. That's a lot of changes, which is error prone. It becomes even more complicated with several properties, which my even depend on each other.
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
}
};
Question: What is a good design for such a program? I'm sure it's possible to do better than the above. The goals are: 1) Make it easy to add/remove such properties or turn on/off their computation 2) Performance is critical. inc
and dec
are called in tight inner loops and do relatively little. They cannot be made virtual for performance reasons.
In reality x
is a more complex data structure. Think e.g. adding/removing edges to/from a graph and keeping track of its degree sequence during the process.
Update
@tobi303 asked that I show how this class would be used. It's in a manner similar to this:
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();
}
}
Or in words:
It's actually a Monte-Carlo simulation similar to a Metropolis-Hasting algorithm.
A concrete example could be where the "data" in class C
(state) is the spin state of an Ising model (for those familiar with it) and properties are the total energy and total magnetization of the system. These are much faster to update after a single spin flip than to recompute from scratch. In practice I don't have an Ising model, I have something a bit more complicated. I have several properties, some fast to compute and some slow (actually I have some auxiliary data structures that help compute properties). I need to experiment with combinations of different properties, so I often change what I include in the code. Sometimes I implement new properties. When I don't need an already implemented property, I need to be able to turn off its computation for performance reasons (some are really slow to compute).
The short form of the design thinking process can be articulated in five steps or phases: empathize, define, ideate, prototype and test.
Design Thinking and Innovation will teach you how to leverage fundamental design thinking principles and innovative problem-solving tools to address business challenges and build products, strategies, teams, and environments for optimal use and performance.
Design thinking is a deeply human process that taps into abilities we all have but get over-looked by more conventional problem-solving practices. Design thinking requires an experimental, collaborative, and optimistic mindset. We define mindset as the ideas and attitudes with which a person approaches a situation.
Just be lazy and don't calculate the properties when you need to. It will remove plenty of code and unnecessary computation.
When you do need your property, compute it if it's not already in cache. So you need a boolean for each property to tell if the cache is up-to-date, and you need to invalidate the booleans each time x
itself is updated.
Basically:
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();
}
};
Edit: an even lazier solution is to instead of storing a boolean in cache
, store an integer correponding to the last update and maintain a counter in C
. Each time the cache is invalidated, the counter in C
is increased. When getting propX
, if the counter doesn't match propX.lastUpdate
then update `propX.
That way, invalidating cache is just one operation and doesn't have to update all the properties' cache.
Here is an approach that may work for you:
class C {
double x;
expC prop1;
someC prop2;
.
.
.
template <typename F>
void for_each_property(const F &f)
{
f(prop1,x);
f(prop2,x,prop1);
.
.
.
}
public:
C() : x(0.0), prop1(x), prop2(x, prop1)
{ }
void inc()
{
x += 1;
for_each_property([](auto &prop,auto&& ...) {
prop.inc();
});
}
void dec()
{
x -= 1;
for_each_property([](auto &prop,auto&& ...) {
prop.dec();
});
}
void set(double x_)
{
x = x_;
for_each_property([](auto &prop,auto&& ... args) {
prop.recompute(args...);
});
}
};
When you add a new property, you only need to add one call in for_each_property()
. The use of variadics avoids the need to provide new overloads for different parameters as long as you stick to the same formula.
This doesn't eliminate the duplication in the constructor, unless you are willing to switch to doing default initialization of the properties and then call set(0.0)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With