Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make C++ class partially constexpr and save RAM

Tags:

c++

c++11

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 Things. The targets are initially assigned at runtime and do not change during runtime.

I need to save memory. As I have ~600 Things, it would help me alot if all those mpTargets 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?

Clarifications

  1. 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.

  2. 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.

  3. 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.

Sample code

#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);
  }
}
like image 445
saum Avatar asked Jan 25 '23 14:01

saum


2 Answers

  1. Use small state
  2. Eliminate all virtual methods (and probably all base classes) to reduce class state.
  3. Use Non-Type template parameters to move references from data (RAM) to code (ROM)

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

like image 23
Mooing Duck Avatar answered Jan 27 '23 05:01

Mooing Duck


Redesign from ground up

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".

One idea: group by properties, not by objects

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:

  • it saves space by abandoning the vtable
  • because how properties are grouped it makes it possible to store the properties known at compile time in ROM
  • it saves space by using the minimum number of bits necessary for each property

Example

For the sake of example let's consider this scenario:

  • we have 100 lamps, 200 buttons and 300 LEDs
  • buttons and LEDs have a target property. Only lamps can be targets
  • buttons have state property. There are 2 possible states (ON/OFF)
  • LEDs have color property. There are 16 possible predefined colors

The following examples use literals to be explicit, but in code you should use constants (e.g. NUM_BUTTONS etc.)

target property

We 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 property

We 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 property

We 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.

Conclusion

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.

like image 100
bolov Avatar answered Jan 27 '23 03:01

bolov