Empty function memory usage

I'm curious, and couldn't find a straight answer with google....

How much memory does an empty function use?

I don't know, but would guess, that the compiler would optimise it out entirely.

But what about cases of 'empty' (so not pure) virtual functions in a base class that is inherited from?

If i had something like:

Foo {
   public:
      virtual void func1(){}
      virtual void func2(){}
      virtual void func3(){}
  // ... etc etc maybe even hundreds
};

// Then

Bar : public Foo {
   public:
//inherits Foo, so 100s of virtual funcs but only overrides 1 or 2
 void func36() override {/* code */ }
};

Given that, what are the implications of creating multiple Bars? Is it wasting swathes of memory with every Bar created?

It can be difficult to impossible to automatically identify dead virtual methods. In the past the linker would include both virtual method tables even if no Foos were created.

The price is the same regardless of the number of Bars created. At least one shared instance of one virtual method table.

If the linker is unable the fold the Bar and Foo method tables then they will both be included.

So, three pointers or six pointers total.

Do you have IDE? make a sketch, compile, write down the memory usage, add 10 empty virtual functions, compile, write down usage, subtract divide by 10, cool?

Only if the code has side effects. If the linker can follow the execution path to any empty method there is a good chance the linker will remove the dead code.

Can you instruct compiler not to touch it?

Printing an incremented global variable is a good choice.

I suspect there are compiler directives that force the linker's hand.

Writing to a volatile will work (like manipulating a pin).

Reading from a volatile should also work (like reading a pin).

Yeah thats what I was unsure of, all the behind the scenes magic that may or may not occur in different scenarios

Only just got around to testing this. Let's see if I did it right...

Just be warned... if you read all this that's your fault :smiley: It's long!

So I have a base class Foo with 20 virtual void funcs. And then a derived Bar class that can choose to implement and override 1 or many of the Foo virtual funcs.

The basic test differences are... Varying amounts of base class functions, varying amounts of derived class functions, and varying amounts of derived class function calls.

So here's the test results and output size code:




One Foo virtual function, one Bar override and one Bar b function call.

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
};

Bar b;

void setup() {
  Serial.begin(9600);
  b.f1();
}

void loop() { Serial.println(x); }
/* Memory Usage
Sketch uses 2072 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/



20 Foo virtual functions, one Bar override and still one Bar b function call:

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
};

Bar b;

void setup() {
  Serial.begin(9600);
  b.f1();
}

void loop() { Serial.println(x); }
/* Memory Usage:
Sketch uses 2072 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/



20 Foo virtual functions, 20 Bar overrides, and still one Bar b function call:

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
    void f2 () override { x  = 2; }
    void f3 () override { x  = 3; }
    void f4 () override { x  = 4; }
    void f5 () override { x  = 5; }
    void f6 () override { x  = 6; }
    void f7 () override { x  = 7; }
    void f8 () override { x  = 8; }
    void f9 () override { x  = 9; }
    void f10 () override { x  = 10; }
    void f11 () override { x  = 11; }
    void f12 () override { x  = 12; }
    void f13 () override { x  = 13; }
    void f14 () override { x  = 14; }
    void f15 () override { x  = 15; }
    void f16 () override { x  = 16; }
    void f17 () override { x  = 17; }
    void f18 () override { x  = 18; }
    void f19 () override { x  = 19; }
    void f20 () override { x  = 20; }
};

Bar b;

void setup() {
  Serial.begin(9600);
  b.f1();
}

void loop() { Serial.println(x); }
/* Memory Usage:
Sketch uses 2072 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/



20 Foo virtual functions, 20 Bar overrides, but 20 Bar b function calls:

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
    void f2 () override { x  = 2; }
    void f3 () override { x  = 3; }
    void f4 () override { x  = 4; }
    void f5 () override { x  = 5; }
    void f6 () override { x  = 6; }
    void f7 () override { x  = 7; }
    void f8 () override { x  = 8; }
    void f9 () override { x  = 9; }
    void f10 () override { x  = 10; }
    void f11 () override { x  = 11; }
    void f12 () override { x  = 12; }
    void f13 () override { x  = 13; }
    void f14 () override { x  = 14; }
    void f15 () override { x  = 15; }
    void f16 () override { x  = 16; }
    void f17 () override { x  = 17; }
    void f18 () override { x  = 18; }
    void f19 () override { x  = 19; }
    void f20 () override { x  = 20; }
};

Bar b;

void setup() {
  Serial.begin(9600);
  b.f1();
  b.f2();
  b.f3();
  b.f4();
  b.f5();
  b.f6();
  b.f7();
  b.f8();
  b.f9();
  b.f10();
  b.f11();
  b.f12();
  b.f13();
  b.f14();
  b.f15();
  b.f16();
  b.f17();
  b.f18();
  b.f19();
  b.f20();
}

void loop() { Serial.println(x); }
/* Memory Usage:
Sketch uses 2300 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/



And so it seems that, at least in preliminary tests... you really do only pay for what you use. If you only use one function, it matters not how many functions are declared/defined inside either base or derived classes. Given the above the only cost is 12bytes per unique function called.

Which makes me wonder about having two derived classes as well. Let's see:



20 Foo virtual functions, 20 Bar overrides, 20 Baz overrides, One Bar b call;

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
    void f2 () override { x  = 2; }
    void f3 () override { x  = 3; }
    void f4 () override { x  = 4; }
    void f5 () override { x  = 5; }
    void f6 () override { x  = 6; }
    void f7 () override { x  = 7; }
    void f8 () override { x  = 8; }
    void f9 () override { x  = 9; }
    void f10 () override { x  = 10; }
    void f11 () override { x  = 11; }
    void f12 () override { x  = 12; }
    void f13 () override { x  = 13; }
    void f14 () override { x  = 14; }
    void f15 () override { x  = 15; }
    void f16 () override { x  = 16; }
    void f17 () override { x  = 17; }
    void f18 () override { x  = 18; }
    void f19 () override { x  = 19; }
    void f20 () override { x  = 20; }
};

class Baz : public Foo {
  public: 
    void f1 () override { x  = 11; }
    void f2 () override { x  = 12; }
    void f3 () override { x  = 13; }
    void f4 () override { x  = 14; }
    void f5 () override { x  = 15; }
    void f6 () override { x  = 16; }
    void f7 () override { x  = 17; }
    void f8 () override { x  = 18; }
    void f9 () override { x  = 19; }
    void f10 () override { x  = 110; }
    void f11 () override { x  = 111; }
    void f12 () override { x  = 112; }
    void f13 () override { x  = 113; }
    void f14 () override { x  = 114; }
    void f15 () override { x  = 115; }
    void f16 () override { x  = 116; }
    void f17 () override { x  = 117; }
    void f18 () override { x  = 118; }
    void f19 () override { x  = 119; }
    void f20 () override { x  = 120; }
};

Bar b;
Baz bz;

void setup() {
  Serial.begin(9600);
  b.f1();
}

void loop() { Serial.println(x); }
/* Memory Usage:
Sketch uses 2072 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
/*



20 Foo virtual functions, 20 Bar overrides, 20 Baz overrides, 1 Bar b call, 1 Baz bz call.

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
    void f2 () override { x  = 2; }
    void f3 () override { x  = 3; }
    void f4 () override { x  = 4; }
    void f5 () override { x  = 5; }
    void f6 () override { x  = 6; }
    void f7 () override { x  = 7; }
    void f8 () override { x  = 8; }
    void f9 () override { x  = 9; }
    void f10 () override { x  = 10; }
    void f11 () override { x  = 11; }
    void f12 () override { x  = 12; }
    void f13 () override { x  = 13; }
    void f14 () override { x  = 14; }
    void f15 () override { x  = 15; }
    void f16 () override { x  = 16; }
    void f17 () override { x  = 17; }
    void f18 () override { x  = 18; }
    void f19 () override { x  = 19; }
    void f20 () override { x  = 20; }
};

class Baz : public Foo {
  public: 
    void f1 () override { x  = 11; }
    void f2 () override { x  = 12; }
    void f3 () override { x  = 13; }
    void f4 () override { x  = 14; }
    void f5 () override { x  = 15; }
    void f6 () override { x  = 16; }
    void f7 () override { x  = 17; }
    void f8 () override { x  = 18; }
    void f9 () override { x  = 19; }
    void f10 () override { x  = 110; }
    void f11 () override { x  = 111; }
    void f12 () override { x  = 112; }
    void f13 () override { x  = 113; }
    void f14 () override { x  = 114; }
    void f15 () override { x  = 115; }
    void f16 () override { x  = 116; }
    void f17 () override { x  = 117; }
    void f18 () override { x  = 118; }
    void f19 () override { x  = 119; }
    void f20 () override { x  = 120; }
};

Bar b;
Baz bz;

void setup() {
  Serial.begin(9600);
  b.f1();
  bz.f1();
}

void loop() { Serial.println(x); }
/* Memory Usage:
Sketch uses 2084 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/



20 Foo virtual functions, 20 Bar overrides, 20 Baz overrides, 20 Bar b calls, 1 Baz bz call

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
    void f2 () override { x  = 2; }
    void f3 () override { x  = 3; }
    void f4 () override { x  = 4; }
    void f5 () override { x  = 5; }
    void f6 () override { x  = 6; }
    void f7 () override { x  = 7; }
    void f8 () override { x  = 8; }
    void f9 () override { x  = 9; }
    void f10 () override { x  = 10; }
    void f11 () override { x  = 11; }
    void f12 () override { x  = 12; }
    void f13 () override { x  = 13; }
    void f14 () override { x  = 14; }
    void f15 () override { x  = 15; }
    void f16 () override { x  = 16; }
    void f17 () override { x  = 17; }
    void f18 () override { x  = 18; }
    void f19 () override { x  = 19; }
    void f20 () override { x  = 20; }
};

class Baz : public Foo {
  public: 
    void f1 () override { x  = 11; }
    void f2 () override { x  = 12; }
    void f3 () override { x  = 13; }
    void f4 () override { x  = 14; }
    void f5 () override { x  = 15; }
    void f6 () override { x  = 16; }
    void f7 () override { x  = 17; }
    void f8 () override { x  = 18; }
    void f9 () override { x  = 19; }
    void f10 () override { x  = 110; }
    void f11 () override { x  = 111; }
    void f12 () override { x  = 112; }
    void f13 () override { x  = 113; }
    void f14 () override { x  = 114; }
    void f15 () override { x  = 115; }
    void f16 () override { x  = 116; }
    void f17 () override { x  = 117; }
    void f18 () override { x  = 118; }
    void f19 () override { x  = 119; }
    void f20 () override { x  = 120; }
};

Bar b;
Baz bz;

void setup() {
  Serial.begin(9600);
  b.f1();
  b.f2();
  b.f3();
  b.f4();
  b.f5();
  b.f6();
  b.f7();
  b.f8();
  b.f9();
  b.f10();
  b.f11();
  b.f12();
  b.f13();
  b.f14();
  b.f15();
  b.f16();
  b.f17();
  b.f18();
  b.f19();
  b.f20();

  bz.f1();
}

void loop() { Serial.println(x); }
/* Memory Usage
Sketch uses 2308 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/



20 Foo virtual functions, 20 Bar overrides, 20 Baz overrides, 20 Bar b calls 20 Baz bz calls

volatile int x = 60;
class Foo {
  public:
    virtual void f1 () {}
    virtual void f2 () {}
    virtual void f3 () {}
    virtual void f4 () {}
    virtual void f5 () {}
    virtual void f6 () {}
    virtual void f7 () {}
    virtual void f8 () {}
    virtual void f9 () {}
    virtual void f10 () {}
    virtual void f11 () {}
    virtual void f12 () {}
    virtual void f13 () {}
    virtual void f14 () {}
    virtual void f15 () {}
    virtual void f16 () {}
    virtual void f17 () {}
    virtual void f18 () {}
    virtual void f19 () {}
    virtual void f20 () {}
};

class Bar : public Foo {
  public: 
    void f1 () override { x  = 1; }
    void f2 () override { x  = 2; }
    void f3 () override { x  = 3; }
    void f4 () override { x  = 4; }
    void f5 () override { x  = 5; }
    void f6 () override { x  = 6; }
    void f7 () override { x  = 7; }
    void f8 () override { x  = 8; }
    void f9 () override { x  = 9; }
    void f10 () override { x  = 10; }
    void f11 () override { x  = 11; }
    void f12 () override { x  = 12; }
    void f13 () override { x  = 13; }
    void f14 () override { x  = 14; }
    void f15 () override { x  = 15; }
    void f16 () override { x  = 16; }
    void f17 () override { x  = 17; }
    void f18 () override { x  = 18; }
    void f19 () override { x  = 19; }
    void f20 () override { x  = 20; }
};

class Baz : public Foo {
  public: 
    void f1 () override { x  = 11; }
    void f2 () override { x  = 12; }
    void f3 () override { x  = 13; }
    void f4 () override { x  = 14; }
    void f5 () override { x  = 15; }
    void f6 () override { x  = 16; }
    void f7 () override { x  = 17; }
    void f8 () override { x  = 18; }
    void f9 () override { x  = 19; }
    void f10 () override { x  = 110; }
    void f11 () override { x  = 111; }
    void f12 () override { x  = 112; }
    void f13 () override { x  = 113; }
    void f14 () override { x  = 114; }
    void f15 () override { x  = 115; }
    void f16 () override { x  = 116; }
    void f17 () override { x  = 117; }
    void f18 () override { x  = 118; }
    void f19 () override { x  = 119; }
    void f20 () override { x  = 120; }
};


Bar b;
Baz bz;

void setup() {
  Serial.begin(9600);
  b.f1();
  b.f2();
  b.f3();
  b.f4();
  b.f5();
  b.f6();
  b.f7();
  b.f8();
  b.f9();
  b.f10();
  b.f11();
  b.f12();
  b.f13();
  b.f14();
  b.f15();
  b.f16();
  b.f17();
  b.f18();
  b.f19();
  b.f20();

  bz.f1();
  bz.f2();
  bz.f3();
  bz.f4();
  bz.f5();
  bz.f6();
  bz.f7();
  bz.f8();
  bz.f9();
  bz.f10();
  bz.f11();
  bz.f12();
  bz.f13();
  bz.f14();
  bz.f15();
  bz.f16();
  bz.f17();
  bz.f18();
  bz.f19();
  bz.f20();
}

void loop() { Serial.println(x); }
/* Memory Usage:
Sketch uses 2514 bytes (0%) of program storage space. Maximum is 253952 bytes.
Global variables use 190 bytes (2%) of dynamic memory, leaving 8002 bytes for local variables. Maximum is 8192 bytes.
*/

So it looks like regardless of what's declared/defined... Using the above code... It's about 12bytes per unique used function. Although the compiler manages to slice a few off here and there with voodoo.

So seemingly that's good news for me...

I have a base (interface) class that could well end up with 100 virtual functions declared but with empty implementations as in the above. Then I have multiple derived classes that maybe override 3 or 5 of the base functions each.

My concern was that every new derived class would be hauling along potentially 100s of 'unused' functions worth of memory.

According to the above, it seems that's not the case. And I'll only use the memory for the functions that are not only overloaded but actually put to use in code.

Ain't compilers, or the folks that wrote them, smart!

Beers all round.

Uh, I hate to mention this. I suspect you're not testing what you think you're testing. Everything is visible to the compiler which allows it to make static calls and eliminate the VMTs. In addition, the compiler can fold the code and eliminate the calls.

A quick check of the disassembly reveals that is exactly what the compiler is doing. All those method calls are reduced to simple assignments to x. Even loop and setup are folded.

And that's precisely why I posted the tests. In case I misunderstood or messed it up.

So what are the requirements to prevent that from happening? What if each implemented function did something like Serial.println(x*analogRead(A0)); (possibly don't even need the x there either)

You will very likely have to access the instance through a pointer to the base class (Bar* test_me = new Foo(); test_me->func1()). I doubt you have to create the instance on the heap; pointing to a static will likely also work.

You will very likely have to use something that cannot be determined at compile time to set your pointer to an instance (like flipping a coin to point to / create either a Foo or a Bar).

You're basically trying to put blinders on the compiler (and linker) so it can't see what you're doing.

What the method does prevents the compiler from eliminating the method. How the method is accessed prevents the compiler from folding the method. Accessing volatile data has you covered for the first one. You now need to stop the folding.