2

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:

  1. after compilation
  2. before setup()
  3. after the "do-nothing" construction of the wrapper
  4. after the "use-space" construction of the wrapper

5 different scenarios were tested:

  1. no pointer declared/wrapper not constructed
  2. pointer declared but object not constructed
  3. do-nothing constructor used
  4. use-space constructor used
  5. 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!

1

I compiled a slightly modified version of your code, with some #defines for selecting which objects get constructed or not. I also printed the available RAM right after entering setup(). I found that, at this point in the program, the version constructing both objects is using 178 bytes more RAM than the version that constructs none.

I then used the commands avr-nm and avr-objdump to try to figure out how the program is using its memory. I found the 178 bytes difference is as follows:

157 bytes of string data
  6 bytes of malloc data
  4 bytes of random state
  2 bytes for the ginny variable
  8 bytes of stack
  1 mystery byte

The difference in string data comes from messages such as “Making Ginormous Class (do nothing constructor)...”, “Deleting first class...”, etc., which are #defined out in the version that does not construct the objects. The malloc() data (heap_start, heap_end and margin) is not used in the version that does not use new, so it is not included in that version. For the same reason, the random() state is not included in the version that does not use random(). The ginny variable, although declared in both versions, is optimized-out by the compiler in the version where it remains unused. The 8 bytes of stack come from the fact that the largest version of setup() needs more CPU register to do its work, and hence pushes more registers to the stack. There is only one byte I could not account for, with value 0, sitting at the end of the .data section, with no associated symbol.

As you can see from this, the difference has little to do with the objects being constructed. It is mostly due to the string data. A smaller part of that difference comes from variables (ginny, random state and malloc() data) which are used only in one version of the program. The practical conclusions are:

  1. If you want to save RAM, use the F() macro when printing constant strings.
  2. If you want to benchmark memory consumption, beware your benchmarking method can have non-obvious effects on the consumption yo are trying to measure.
0

I have to confess that I probably doesn't understand what you are asking ...

Regarding the stats giving by the compiler (tested on a Uno, Arduino IDE 1.8.5), I got

Global Wrapper (using global ginny)

Sketch uses 3304 bytes (10%) of program storage space. Maximum is 32256 bytes.
Global variables use 400 bytes (19%) of dynamic memory, leaving 1648 bytes for local variables. Maximum is 2048 bytes.

Execution

Initial free RAM: 1640
Making Ginormous Class (do nothing constructor)...
Current free RAM: 1636

Deleting first class...
Current free RAM: 1640

Making Ginormous Class (use a ton of RAM constructor)...
Current free RAM: 610

Deleting second class...
Current free RAM: 1640

Local Wrapper (using local ginny)

Sketch uses 3258 bytes (10%) of program storage space. Maximum is 32256 bytes.
Global variables use 398 bytes (19%) of dynamic memory, leaving 1650 bytes for local variables. Maximum is 2048 bytes.

Execution

Initial free RAM: 1642
Making Ginormous Class (do nothing constructor)...
Current free RAM: 1638

Deleting first class...
Current free RAM: 1642

Making Ginormous Class (use a ton of RAM constructor)...
Current free RAM: 612

Deleting second class...
Current free RAM: 1642

Local Wrapper version uses 2 bytes less of global space. That's correct, because space for local ginny comes from the stack, while global ginny comes from the heap.

No compiler will give you how much space (for variables) will you need during execution. That's unsolvable (from Turing's Halting Problem). Just think what happens when you enter in a non-ending recursion.

Comparing execution results, both version show the same amount of memory, off by two bytes, which is due to ginny declared global vs local.

There is a differente between free space declared by compiler (1648, 1650 bytes) and space reported during program execution (1640, 1642 bytes). The difference is 8 bytes in both cases.

Now, setup is a function called from main. That's means you need stack space for the call (return address and whatever), which explains the 8 bytes difference.

0

I am assuming that the RAM given by the compiler is fact ...

That's about the only thing I wouldn't assume. The compiler can't really know how much RAM your code is going to use, especially when you do dynamic memory allocation. The compiler can certainly report on how much space your global variables take, as it has to allocate addresses for them.

I have a post about what RAM is used in a minimal sketch, for example this:

#include "memdebug.h"

void setup ()
  {
  Serial.begin (115200);
  Serial.println ();
  Serial.print (F("Free memory = "));
  Serial.println (getFreeMemory ());
  }  // end of setup

void loop () { } 

Perhaps surprisingly, that sketch already uses 346 bytes of RAM, and it hasn't got any (visible) variables in it.


what can I do to reduce it?

  Serial.println("Making Ginormous Class (do nothing constructor)...");

You could start by using the F macro:

  Serial.println(F("Making Ginormous Class (do nothing constructor)..."));

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.