Problem
I have a few large objects in my code that need global visibility, but creating them seems to eat up memory significantly faster than simple math would dictate, and furthermore, using them differently in code seems to change how much RAM the compiler tells me I have left for local variables.
How do I optimize the use of my RAM while still being able to use an object in both my setup()
and my loop()
functions?
Note: Skim the Pseudo-Code section and then skip to the Discussion section if want to get to the meat of my question (I just wanted to provide plenty of background, if needed).
Test Code
To demonstrate that reported global RAM usage changes due not only the number of variables I define globally, but how those variables are used in code, I created a test program. I compiled this code on the Arduino IDE on a Mac, and ran this on multiple Elegoo Nanos.
Please forgive the particularly long "minimal" complete verifiable code, but it has a bit going on. Here's the pseudo-code for it.
Pseudo-Code
"T" (for tested) before a line means that I commented out a line or left it be depending on what test I was running:
Define a function to check how much memory remains, freeRam() (taken from https://playground.arduino.cc/Code/AvailableMemory, bottom of page)
Create a class that uses a ton of RAM, about 1024B (GinormousClass)
Create a wrapper for GinormousClass (GinormousClassWrapper) that may or may not construct the GinormousClass, based on how it is implemented
Global scope:
Run freeRAM() to see how much initial RAM is available before setup() is run.
T Create a global pointer to the wrapper class
Local scope (setup):
Report initial RAM
T Create a local pointer to the wrapper class
T Assign a newly constructed wrapper object to the pointer, which does NOT instantiate the huge class
Report how much RAM remains after creation and after deletion
T Assign a newly constructed wrapper object to the pointer, which DOES instantiate the huge class
Report how much RAM remains after creation and after deletion
Note: Remaining RAM before creation and after deletion were always the same.
Actual Code
#define ARRAY_SIZE 256
// Method to determine how much memory is left (https://playground.arduino.cc/Code/AvailableMemory, bottom of page)
int freeRam() {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
};
// Define a class that uses a ton of RAM
class GinormousClass {
public:
GinormousClass() { // Fill array with (kind of) random numbers
for (int i = 0; i < ARRAY_SIZE; i++) {
BIG[i] = random(1000);
}
};
long BIG[ARRAY_SIZE]; // Have this class control a huge array
};
// Define a class that has the potential to use a ton of RAM
class GinormousClassWrapper {
public:
GinormousClassWrapper(bool createGiantObject) { // Constructor
if (createGiantObject) {
// Create a GinormousClass object (use a ton of RAM)
gPointer = new GinormousClass();
} else {
// Do not create GinormousClass object
}
};
~GinormousClassWrapper() { // Delete the Ginormous Class, if it exists
if (gPointer) {
delete gPointer;
}
}
GinormousClass *gPointer = NULL; // Initialize pointer to NULL
};
int initialFreeRam = freeRam(); // Figure out how much RAM is available before the fun begins
GinormousClassWrapper *ginny; // Declare a GLOBAL pointer to the potentially enormous class
void setup() {
// Start the Serial connection, and get the initial amount of RAM
Serial.begin(9600);
Serial.print("Initial free RAM: ");
Serial.println(initialFreeRam);
// GinormousClassWrapper *ginny; // Declare a LOCAL pointer to the potentially enormous class
// Try making the class, without defining the array
Serial.println("Making Ginormous Class (do nothing constructor)...");
ginny = new GinormousClassWrapper(false);
Serial.print("Current free RAM: ");
Serial.println(freeRam());
Serial.println();
// Delete the class
Serial.println("Deleting first class...");
delete ginny;
Serial.print("Current free RAM: ");
Serial.println(freeRam());
Serial.println();
// Try making the class, WITH defining the array
Serial.println("Making Ginormous Class (use a ton of RAM constructor)...");
ginny = new GinormousClassWrapper(true);
Serial.print("Current free RAM: ");
Serial.println(freeRam());
Serial.println();
// Delete the class
Serial.println("Deleting second class...");
delete ginny;
Serial.print("Current free RAM: ");
Serial.println(freeRam());
Serial.println();
};
void loop() {
};
Results
I've collected the RAM reported at 4 different times:
- after compilation
- before
setup()
- after the "do-nothing" construction of the wrapper
- after the "use-space" construction of the wrapper
5 different scenarios were tested:
- no pointer declared/wrapper not constructed
- pointer declared but object not constructed
- do-nothing constructor used
- use-space constructor used
- do-nothing constructor used, object deleted, and use-space constructor used
Raw Results:
Numbers are amount of RAM left, in bytes
0 Not declared:
Compiler: 1836
Initial: 1830
Do-nothing constructed: N/A
Use-space constructed: N/A
1 Declared global, not constructed:
Compiler: 1836
Initial: 1830
Do-nothing constructed: N/A
Use-space constructed: N/A
2 Declared global, constructed do-nothing:
Compiler: 1734
Initial: 1728
Do-nothing constructed: 1724
Use-space constructed: N/A
3 Declared global, constructed use-space:
Compiler: 1722
Initial: 1716
Do-nothing constructed: N/A
Use-space constructed: 686
4 Declared global, constructed both:
Compiler: 1648
Initial: 1640
Do-nothing constructed: 1636
Use-space constructed: 610
Note: Local pointer instead of global pointer had no change except increase all reported RAM by 2 bytes.
Discussion
In the raw results, the GinormousClassWrapper takes 4B when constructed (the pointer it owns) and GinormousClass takes about 1024B when constructed (exactly 1030B, we can assume there's some overhead). However, one strange thing is of note:
The amount of room stated by the compiler to be available for local variables decreases significantly when the wrapper is constructed versus when it is not (1836B down to 1640B when both constructors are used). I would expect that the amount of RAM used by globals would not change, because technically only the pointer is in global scope. If it is SUPPOSED to change according to how the pointer is used, then why does it change by 196B, of all numbers?
I am assuming that the RAM given by the compiler is fact, and it's one that I would like to know because my project is running on tight memory restrictions. So where is all of this valuable extra RAM going, and what can I do to reduce it? Thank you for any help!