Using Classes increases Flash memory significantly

While this question/observation arose from a specific program (though single and first occasion), I'd like to discuss this at a more generic level (if at all possible). While I have been coding for a while, I am new to writing classes. Hence, I am a procedural rather than object-oriented guy. :slight_smile:

The trigger:
I refactored a program I wrote two years ago over the last three days, adding my first ever written classes (one for controlling relays, another for LEDs).

Background:
The overall scenario is controlling a garage with three bays, each having a roller door and ceiling lights, thus have three boxes with four push buttons, one at each pillar next to a roller door.

 _______      _______      _______ 
|       | :: |       | :: |       | ::* 
|       | 1  |       | 2  |       | 3

Button arrangement :: (dots representing push buttons)
1 3 (light)
2 4 (roller door)

* (the two extra buttons at box 3, switch two other lights, so disregard these for the time being)

Box 1 and 2 can switch the bay to the left and right.

My initial program basically had push buttons sending a individual voltages to an analogue pin. The program would allocate a sequential button number:
Box 1 switch 1..4, to a button id of 1..4
Box 2 switch 1..4, to a button id of 5..8
Box 3 switch 1..4, to a button id of 9..12

And we end up with:

 1  |  2  |   3   | Box
1 3 | 5 7 |  9 11 | light
2 4 | 6 8 | 10 12 | roller door

A switch statement would trigger the respective relay for a roller door or light.
case 1 -> light 1 ON
case 3, 5 -> light 2 ON
case 7, 9 -> light 3 ON

... roller doors accordingly...

Without complicating this further...

I had (in the non-class code) two functions switching the individual (light and roller door) relays ON or OFF via their button_id.
Because I am communicating the relay states to an automation system, which can also control the relays, the sequential number was ideal to track the relay state with bit setting and bit reading operations (A byte for lights, another for roller doors).

The MQTT message structure is [R|L][1..4][0|1]; e.g., L11 for light 1 ON.

If you could follow so far (better than 1,500 lines of code :slight_smile: ) this seems straight forward.

Now... I replaced the relays for both lights and roller doors with a Relay class.

class Relay
{
    public: // called access modifier

        // Constructor
        Relay(uint8_t pin_relay);
        Relay() {}; // do not use

        // member methods

        // Normally a relay is set HIGH for it to be ON, and LOW for OFF
        // SaintSmart branded relay boards work the opposite: HIGH = OFF
        void init(bool default_low_for_off);
        void on();
        void off();
        void toggle();

        // Setter
        void setState(bool desired_state);

        // Getter
        bool getState();

    private: // called access modifier
        uint8_t m_pin_relay;
        bool m_default_low_for_off;
        bool m_state;
};

I have instantiated these relays like so:

Relay relay_rollerdoor_south(PIN_ROLLERDOOR_SOUTH);
Relay relay_rollerdoor_centre(PIN_ROLLERDOOR_CENTRE);
Relay relay_rollerdoor_north(PIN_ROLLERDOOR_NORTH);

Relay relay_lights_south(PIN_LIGHTS_SOUTH);
Relay relay_lights_centre(PIN_LIGHTS_CENTRE);
Relay relay_lights_north(PIN_LIGHTS_NORTH);

Relay relay_lights_external_1(PIN_LIGHTS_EXTERNAL_1);
Relay relay_lights_external_2(PIN_LIGHTS_EXTERNAL_2);

Roller door relays are also treated differently than light relays, because lights are truly ON|OFF, while roller doors only need a momentary signal to toggle UP|DOWN. For the latter an auto OFF is sent to the relay 400 ms after it received an ON.

The difference in code is now (top with classes, bottom old code no classes)

    20240723-2356 v0.2.0 w/o DEBUG
    RAM:   [=         ]  11.8% (used 970 bytes from 8192 bytes)
    Flash: [=         ]  10.8% (used 27422 bytes from 253952 bytes)

    20240720-0900 v0.1.3 w/o DEBUG
    RAM:   [=         ]  11.1% (used 913 bytes from 8192 bytes)
    Flash: [=         ]  10.4% (used 26412 bytes from 253952 bytes)

While I could squeeze the bottom (non-class code) into an UNO, the upper has to go to a MEGA.

Now, I am not sure, whether the information provided can lead to the confirmation that classes require more flash or not.

But as a more generic question: Have experienced programmers seen something similar? (... that the use of classes can increase the flash memory requirement significantly.)

I usually try to avoid strings, and use F(), and the smallest size variable to actively save memory and squeeze the most into the UNO.

Any hints/commentary appreciated.

Thank you for reading through my post.

I understand what you're saying.

Though I am a bit disappointed that my more 'contemporary' programming style now hampers the space saving. On the other hand, maybe it is time to replace the UNO with something else.

Most of my controllers are UNOs, but I wish there was a UNO form factor with same I/Os, but with the MEGA chip on it. :slight_smile: All of my controller are wired Ethernet connected.

Time for a rethink, I think...

How much flash memory does the code take when compiled for an UNO? Seems odd that it would take less on a Mega, and 27422 bytes is only 85% of the flash on the UNO.

Are you really considered that 3% difference as significant?
I think, a simple rewriting your procedural code a slight different way could give you a similar result.

Why you told nothing about significant ( 6% ) decrease of using RAM? On Mega with it limited memory resources I would say it way more important.

Nope, but now I have... very good replacement indeed! Thank you.
Ordered both, WiFi and non-WiFi versions. :slight_smile:

Both memory lists are from the same MEGA.

No, it's not about percent, but the 1k.


In any case, given my inexperience with classes, I might be chasing a ghost.
However, I perceive my relay class as an improvement.

Probably most of the memory is used by the libraries. But if you are so memory restricted maybe to build classes is not the best option, specially for such simple programs and restricted (an old) MCU's.

A way in the middle could be to use structs. Something like this (not tested):

#define ROLLERDOOR_SOUTH  0
#define ROLLERDOOR_CENTER 1
#define ROLLERDOOR_NORTH  2
//...
struct RelayType { 
    uint8_t pin;
    bool default_state;  // low for off
    bool state; 
}; 

RelayType relay[] =
{
    { 2,  OFF, OFF},  // ROLLERDOOR_SOUTH
    { 5,  OFF, OFF},  // ROLLERDOOR_CENTER
    { 21, ON,  OFF},  // ROLLERDOOR_NORTH
    ...
};

// to access the relays:
  relay[ROLLERDOOR_CENTER].state = ON;
  relay[ROLLERDOOR_NORTH].default_state = OFF;
  relay[ROLLERDOOR_CENTER].pin ...

This would save most of the class overhead.
Then build simple pure functions that get always just a relay struct as parameter.
You could even use just an Nx3 array instead of structs:

 #define PIN 0
 #define DEF_STATE 1
 #define STATE 2

 bool relay[N][3];  // or initialize here

 relay[ROLLERDOOR_CENTER][DEF_STATE] = OFF;
 relay[ROLLERDOOR_CENTER][STATE] = ON;
 relay[ROLLERDOOR_CENTER][PIN] ...

EDIT: I added also the PIN

This would use the minimum memory.
You could also consider other MCU with much more memory, like ESP32. There are dev boards with ethernet also and smaller.

You could use bit arrays instead of bool arrays :slight_smile:

There are at least two learnings I had:

  • The compiler's optimization for AVRs works better without classes . For example constant class members mostly end up in SRAM, whereof in a procedural sketch I can force them with constexpr to stay in Flash only.
  • OOP adds some overhead for the addressing the member functions

I tried two very similar sketches:

Sketch procedural
/*
  Compare Procedural Example
  constexpr
  Sketch uses 1512 bytes (0%) of program storage space. Maximum is 253952 bytes.
  Global variables use 9 bytes (0%) of dynamic memory, leaving 8183 bytes for local variables. Maximum is 8192 bytes.


*/
void relayOn(const uint8_t pin, const int8_t active = HIGH) {
  digitalWrite(pin, active);
}

void relayOff(const uint8_t pin, const int8_t active = HIGH) {
  digitalWrite(pin, !active);
}

void relayBegin(const uint8_t pin, const int8_t active = HIGH) {
  relayOff(pin, active);
  pinMode(pin, OUTPUT);
}

void relayToggle(const uint8_t pin, const int8_t active = HIGH) {
  if (digitalRead(pin) == active)
    digitalWrite(pin, !active);
  else
    digitalWrite(pin, active);
}


// 1512/9
constexpr uint8_t relayPinA {2};
constexpr uint8_t relayPinB {3};
constexpr uint8_t relayPinC {4};

constexpr int8_t relayActiveA {HIGH};
constexpr int8_t relayActiveB {HIGH};
constexpr int8_t relayActiveC {LOW};

/*
// 1516/9
uint8_t relayPinA {2};
uint8_t relayPinB {3};
uint8_t relayPinC {4};

int8_t relayActiveA {HIGH};
int8_t relayActiveB {HIGH};
int8_t relayActiveC {LOW};
*/

void setup() {
  relayBegin(relayPinA, relayActiveA);
  relayBegin(relayPinB, relayActiveB);
  relayBegin(relayPinC, relayActiveC);

  relayOn(relayPinA, relayActiveA);
  relayOff(relayPinB, relayActiveB);
  relayToggle(relayPinC, relayActiveC);
}

void loop() {

}
//

which compiles on a Mega with:

  Sketch uses 1512 bytes (0%) of program storage space. Maximum is 253952 bytes.
  Global variables use 9 bytes (0%) of dynamic memory, leaving 8183 bytes for local variables. Maximum is 8192 bytes.

The 6 "variables" don't need any SRAM (Just as a note - an empty sketch already needs 9 Bytes of SRAM).

A similar sketch in OOP:

Sketch OOP
/*
    Compare OOP Example
    
Sketch uses 1656 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 15 bytes (0%) of dynamic memory, leaving 8177 bytes for local variables. Maximum is 8192 bytes.

 
 */

class Relay {
    protected: 
        const uint8_t pin;
        const int8_t active;
        
    public: 
        Relay(const uint8_t pin, const int8_t active = HIGH) : 
          pin(pin), active(active) {}

        void on() {
          digitalWrite(pin, active); 
        }
        
        void off() {
          digitalWrite(pin, !active);
        }

        void begin() {
          off();
          pinMode(pin, OUTPUT);
        }
        
        void toggle() {
          if (digitalRead(pin) == active) 
            off();
          else 
            on();
        }
};

Relay relayA(2);
Relay relayB(3);
Relay relayC(4, LOW);

void setup() {
  relayA.begin();
  relayB.begin();
  relayC.begin();

  relayA.on();
  relayB.off();
  relayC.toggle();
}

void loop() {
  
}
//

which needs

Sketch uses 1656 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 15 bytes (0%) of dynamic memory, leaving 8177 bytes for local variables. Maximum is 8192 bytes.

here the 6 member variables are held in SRAM also. If you add an instance (and use it) or if you change the variable size in the class - you will see the change of SRAM usage.

Nevertheless I try to solve problems mostly with OOP.

btw:

that's fine - but remember: you don't get any money back for unused Flash/SRAM.

if you compile your 2 codes for ESP32 you'll see that the SRAM use is the same and you save a few flash bytes (237353 versus 237361) with the OO code.

OK - yes I'm sure you did the test too :slight_smile:

just wanted to put that out there that on more modern platforms OOP is not "increasing flash memory significantly" when done correctly.

(side note: you issue a digitalWrite to a pin before setting it as an OUTPUT) in your relayBegin() function. as the pin are input initially, on AVR setting them HIGH will turn the pullup on — not sure it's an issue or not.)

Well, that's not the whole truth: You compare "g++ handling mixed C/C++" with "g++ handling C++" - I'd be surprised if there were huge differences as the C++ bookkeeping is in the code anyway. It is more interesting to compare "g++ handling plain C" with "gcc handling plain C" - which is no problem to do on AVR.

Is there somewhere the full code has been posted? I can't see where you got that information from the code posted in this discussion?

Don't forget that the compiler does not optimize for multiple occurrences of identical text when using F().

My question was how much memory it took when compiled for an UNO, since you stated it did not fit, but it seems odd that compiling on a Mega reduces flash usage by nearly 15%.

there could be other posts with snippets; however, my idea was to keep this as generic as possible, rather then make it it a code analysis exercise.
Form what I gather from the posts, using classes will increase memory size, and this confirms, my, arguably anecdotal, experience.

Yes, and I use words to form bigger strings, rather than storing longer sentences.

Not sure where this is coming from; I am solely talking about compilation on the MEGA (non-OO vs. OO code).

I mentioned the UNO, by insinuating that 1KB difference could well kill the option of using an UNO, if OO code blows up memory use by 1KB.

But to satisfy your curiosity, on the UNO we get this:

RAM:   [=====     ]  46.0% (used 942 bytes from 2048 bytes)
Flash: [========  ]  82.3% (used 26548 bytes from 32256 bytes)

I have been running into trouble when using almost all flash memory.

I hear you... why I immediately ordered some R4s when Delta_G mentioned it in post #4.

Yes, preventing the relays from briefly going to ON at reboot.


Again, thank you for all the comments.

This is a known method of preventing a low switched relay from clicking at startup.
If done traditionally,

pinMODE(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH);

then between the first and second lines the relay will briefly have a low level and it will turn on.

If you do it in reverse, then immediately after switching on there will be HIGH.

I thought the main problem was that you wanted this to fit on an UNO, and it would no longer do that:

wouldn't the pullup be still activated though (on AVR)?