Understanding Memory using Embedded C++
C++ is quickly becoming a powerful tool for microcontroller programming. A space previously occupied solely by C and assembly languages. However, many firmware engineers fear using C++ because they worry it uses up too much memory (both RAM and flash). Let’s look at how C++ generates code and utilizes RAM.
Read More. Understanding RAM/Flash usage in embedded C
The full power of C++ is not realized on embedded microcontrollers but rather “Embedded C++” which I define as stripping away exception handling, dynamic typing, as well as a few other resource-intensive features of C++. What is left is a powerful embedded programming language that allows for much better code re-use than plain old C.
Code Generation
From a code generation viewpoint, embedded C++ has a few differences from C, namely: namespaces, classes, constructors, destructors, and inheritance. Let’s look at namespaces and classes first.
Namespaces and Classes
In C, when you create a function with a global scope called read_spi
, the C compiler creates a function called read_spi
. However, when you create a static function called read_spi
, the C compiler does some mangling and will rename the function to be something like read_spi_local_234
. Similar to how C uses the static
keyword to limit the scope of a function, C++ namespaces and classes limit the scope of functions and methods. C++ then mangles all methods and functions so that they are unique within a global scope (such as an object or binary file). When C++ links to C code that is designated as extern "C"
, the C function is not mangled. Mangling only affects intermediate files (e.g., object files). When the final code is converted to a machine-readable binary, the names of functions are completely removed. The machine uses addresses rather than function names. Ultimately, namespaces and classes don’t affect the size of the generated code.
Here is a code snippet of some compiler-generated code that illustrates mangling (comments added):
de0001d0 <_ZN3var6StringD0Ev>: //Method from String class that has been mangled
de0001d0: b510 push {r4, lr}
de0001d2: 4b04 ldr r3, [pc, #16] ; (de0001e4 <_ZN3var6StringD0Ev+0x14>)
de0001d4: 4604 mov r4, r0
de0001d6: 4798 blx r3
de0001d8: 4620 mov r0, r4
de0001da: 4b03 ldr r3, [pc, #12] ; (de0001e8 <_ZN3var6StringD0Ev+0x18>)
de0001dc: 4798 blx r3
de0001de: 4620 mov r0, r4
de0001e0: bd10 pop {r4, pc}
de0001e2: bf00 nop
de0001e4: de0001b9 .word 0xde0001b9
de0001e8: de0009d1 .word 0xde0009d1
de0001ec <main>: //Raw C function name with a global scope
de0001ec: e92d 41f0 stmdb sp!, {r4, r5, r6, r7, r8, lr}
de0001f0: b098 sub sp, #96 ; 0x60
de0001f2: 460a mov r2, r1
You can try demangling the code above using this tool.
Constructors and Destructors
Constructors and destructors don’t typically have a big impact on flash memory but can raise some questions for RAM usage which will be discussed later.
Inheritance
Inheritance is a powerful mechanism in C++ that makes re-using code from similar objects seamless; however, it can cause the flash to be peppered with additional code especially if virtual
methods are used.
Here is an example:
class A { public: virtual int do_something(); }
class B : public A { public: int do_something(); }
The method do_something()
in class B will override the method do_something()
in class A for objects of type B. However, even if no objects of class A are ever created, the compiler will likely generate code for do_something()
from class A. The reason is that the compiler isn’t quite sophisticated enough to remove the code because it assumes do_something()
from class A may be called through dynamic typing (which is usually disabled when using embedded C++).
When using virtual
methods, the compiler also generates tables of pointers that allow the code to resolve the correct method to call based on the class. These tables are called vtables
. As a result, you will want to always consider the implementation and decide if a particular method is worth virtualizing. Some virtual
methods provide big code-quality advantages and are definitely worth the minor penalty.
The power that inheritance brings certainly outweighs the minor increases in code size which are easily minimized by writing good embedded C++ code.
RAM Usage
Constructors and Deconstructors (Again)
There is no ambiguity about RAM usage when declaring variables in C. This is not the case for C++. Consider the following examples.
C:
uint32_t num;
C++:
MyClass object;
In the C example, declaring num
will use sizeof(uint32_t)
bytes of RAM either on the stack or heap depending on the scope. However, in C++ when a class object is declared it takes up sizeof(MyClass)
plus it executes the MyClass
constructor.
While sizeof(Myclass)
is known to the compiler, the size isn’t readily apparent to the programmer. This isn’t a problem if MyClass
was designed for memory-constrained systems. But there is another challenge.
The constructor has the opportunity to execute any code including using dynamic memory allocation. If MyClass
dynamically allocates memory in a way that you are not expecting, it could cause problems. Again, this can be managed by using well-written embedded C++ code.
Deconstructors will typically be used to free memory that is dynamically allocated by constructors. Embedded systems will quickly run out of memory if there are leaks. This typically presents as buggy behavior that is difficult to debug.
Keywords new and delete
The new and delete keywords can cause some angst among memory-conscious C programmers. They shouldn’t. They are simply an easy way for C++ programs to invoke malloc()
and free()
. My default approach is to avoid using new
and delete
in embedded C++ frameworks. This allows the application developer to decide whether or not to use dynamic memory allocation.
Final Thoughts
After better understanding code and RAM usage in C++, I grew to love developing embedded applications in C++ because of the powerful nature of inheritance and object-oriented programming. However, C is still my programming language of choice when it comes to system and kernel development.