I have written code for a controller that has buttons and lamps. It is based on Arduino/ATmega2560. RAM is very limited. Changing to another device is no option.
Each button and lamp is modelled as a C++ class instance. They are derived from an abstract class Thing
, which contains a pointer mpTarget
that indicates connections between Thing
s. The targets are initially assigned at runtime and do not change during runtime.
I need to save memory. As I have ~600 Thing
s, it would help me alot if all those mpTarget
s were stored in ROM (and not in RAM), because the targets are known at compile time.
So I played with constexpr
. However, it seems to be impossible to make a class only partially constexpr
. I cannot make the whole class constexpr
, because there are also member variables that change during runtime (mState
in the example below).
What would be a good way to achieve this behavior? constexpr? Templates? Anything?
I know that mpTarget
is currently initialized at runtime. But each target is known at compile time, that's why I'd like to find a good way to save RAM bytes of these pointers.
I don't really want to move away from object oriented design. I used an approach with simple, "plain old" datatypes before (which actually consumed less RAM). But as development of this code is still ongoing (classes and instances are added/deleted/modified), there is a high chance to miss necessary modifications, introducing hard-to-find bugs.
For this project, it would be more than enough to save the RAM storage only for mpTarget
. Keeping the overhead of vtable pointers is fine. What I'm actually looking for is - as the title suggests - a way how we could implement a class whose members are partially constexpr
. I also thought about using templates, but without success.
#include <iostream>
class Thing
{
public:
// only called in setup()
void setTarget(Thing & pTarget)
{
mpTarget = & pTarget;
}
virtual void doSomething(int value) {};
protected:
// known at compile time - can we make this constexpr?
Thing * mpTarget;
};
class Button : public Thing
{
public:
void setState(int newState)
{
mState = mState + newState;
mpTarget->doSomething(mState);
}
private:
// changes during runtime
int mState;
};
class Lamp: public Thing
{
public:
virtual void doSomething(int value)
{
std::cout << value << std::endl;
};
};
Button myButton;
Lamp myLamp;
int main()
{
myButton.setTarget(myLamp);
while (1)
{
myButton.setState(123);
}
}
Use an enum
based on a char for state when possible:
enum binary_enable_state : char {
BINARY_ENABLE_ON,
BINARY_ENABLE_OFF
};
Event listeners should just listen, and have no pointers or references or any real state.
class Lamp {
public:
void doSomething(binary_enable_state value)
{
std::cout << value << std::endl;
};
};
Event sources should uses non-type template parameter references to the listeners, thus avoiding pointers and state.
template<class Listener, Listener& mpTarget>
class Button {
public:
void setState(binary_enable_state newState)
{
mState = newState;
mpTarget.doSomething(mState);
}
private:
// changes during runtime
binary_enable_state mState; //Do you actually need to store this?
};
This basically puts the graph in the code itself (ROM), rather than as data in RAM.
Lamp myLamp;
Button<decltype(myLamp), myLamp> myButton;
int main()
{
myButton.setState(BINARY_ENABLE_ON);
}
http://coliru.stacked-crooked.com/a/7171e4b9547ccbe1
ATmega2560 has just 8KB SRAM. This is extremely low compared to normal, desktop standards. 600 objects with 3 2-byte properties each would fill almost half of the available memory.
Programming on such a restrictive environment forces you to adapt your whole design from the start around the hardware limitations. Writing normal code and then trying to fit it to your hardware after the fact just doesn't cut it here. This is a good exception to the "first write readable code, then try it to optimize it".
First you need to abandon virtual methods. That would add at least a vtable pointer per instance. At 600 instances it is a very heavy cost.
One ideea I have for a design here is to ditch the OOP completely. Or at least partially. Instead of properties grouped per instance, group all properties together in vectors.
This has a few big advantages:
For the sake of example let's consider this scenario:
target
property. Only lamps can be targetsstate
property. There are 2 possible states (ON/OFF)color
property. There are 16 possible predefined colorsThe following examples use literals to be explicit, but in code you should use constants (e.g. NUM_BUTTONS
etc.)
target
propertyWe have 500 (200 buttons + 300 LEDS) objects with target
property. So we need a vector of size 500
. There are 100 targets. So the data type fits in a int8_t
. So target looks like this:
constexpr int8t_t targets[500] = ...
In this vector the first 200 elements represent the targets of the buttons and the next 300 elements represent the targets of the LEDs. This vector will be stored in ROM.
To get the target of a thing we have:
int8_t button_target(int button) { return targets[button]; }
int8_t led_target(int led) { return targets[200 + led]; }
Alternatively use two vectors:
constexpr int8t_t button_targets[200] = ...
constexpr int8t_t led_targets[300] = ...
Populating this vector at compile time is a problem you need to solve. I don't know exactly how you are creating your objects now. You could hard code the values in the code or you could generate the code.
state
propertyWe have 200 elements with state. Since there are 2 possible states we just need 1 bit per state. So we just need 200 / 8 = 25
bytes:
int8_t state[25] = {};
Getting and setting the state of a button is more complicated, it requires bitmask operations, but we have saved 175 bytes here (87.5% saved space on this data) and every byte matters.
bool button_get_state(int button)
{
int8_t byte = state[button / 8];
return byte & (1 << (button % 8));
}
color
propertyWe have 300 elements with color. Since there are 16 colors we need 4 bits per color so we can encode 2 colors per byte:
int8_t color[150] = {};
Again getting and setting color requires bit fiddling.
As you can see this design is by far not as pretty as OOP. And is it requires more complex code. But it has the big advantage that is saves you a lot of space, which is paramount on a device with just 8,192 bytes
of SRAM.
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