8

While doing big projects using Arduino boards (Uno, Atmega328P MCU). I used to get warnings like this

Sketch uses 13764 bytes (44%) of program storage space. Maximum is 30720 bytes.
Global variables use 1681 bytes (82%) of dynamic memory, leaving 367 bytes for local variables. Maximum is 2048 bytes.

Low memory available, stability problems may occur.
  • What are the generally practiced methods for the optimization program memory usage?
  • Is there any difference in memory usage if the variable is declared globally or locally.
  • Will it matter what the control statement/selection statements are (like if, switch )
  • Usage of Serial monitor. Serial.print()
  • ......

Low memory available, stability problems may occur.

How bad are these warnings?

Before marking it duplicate, I have referred the following. But it wasn't satisfactory
Most memory efficient way to program
What are safe memory usage limits?

1
  • not a traditional way, but Nano Every has 3 times the SRAM of classic Nano and the updated compiler doesn't copy constant strings into SRAM.
    – Juraj
    Commented Jul 1, 2020 at 11:26

4 Answers 4

18

What are the generally practiced methods for the optimization program memory usage?

First, note you are searching for ways to lower SRAM memory. This contains global (variable) memory and heap space (dynamic memory + stack memory).

  • Avoid memory gaps, by not using dynamic memory (with free/malloc/new).
  • Avoid using the String class.
  • Avoid global memory in SRAM by using PROGMEM, F(..) if possible.
  • Use the smallest variable size (e.g. uint8_t instead of int).
  • Store arrays of booleans into bits (8 booleans per byte).
  • Use bit flags if applicable.
  • Use some compressed type of memory internally (affects performance), e.g. if you have many 6 bit values to store, store them not in separate bytes but use 6 bytes for 8 times 6 bit values).
  • Avoid passing arrays by value.
  • Avoid a deep call stack with many (big) variables. Note this affects your design, so use it as a last resort.
  • If you need a calculatable table, calculate each value instead of storing it as a table.
  • Do not use longer arrays than needed, and think about reasonable maximum array sizes otherwise (see hcheung's comment below).
  • (this is not a complete list).

Is there any difference in memory usage if the variable is declared globally or locally.

Yes, local variables are added to the stack, but are removed after the function ends, global variables stay (but are only created once). Note that variables on the stack (and also dynamic memory) are NOT taken into account in the memory calculated in the warning message during compiling.

Will it matter what the control statement/selection statements are (like if, switch )

No, this will only affect the program memory.

Usage of Serial monitor. Serial.print()

Probably yes, the serial monitor probably reserves (quite?) some memory as a buffer.

Low memory available, stability problems may occur. How bad are these warnings?

How bad it is, depends on how much memory is used which is not calculated, which is dynamic memory, and stack memory.

You can calculate it manually (which can be quite cumbersome for a big program), you can also use the GitHub library for this:

Arduino MemoryFree

If you know how much heap memory you use worst case, than add it to the calculated global variables memory. If this is less than your maximum available SRAM memory, you are safe.

10
  • 1
    Upvote for the comprehensive summary. I'd like to add one more "allocate your array size according to the need", I often see many ArduinoJson users created an allocation as large as 1024 bytes with StaticJsonDocument<1024> doc; for a target data object consists of only a couple of key/value pairs that take no more than 50 bytes.
    – hcheung
    Commented Jul 1, 2020 at 14:01
  • 1
    What about macros like #define Commented Jul 1, 2020 at 16:00
  • 2
    Nitpick: I would say "avoid using String" rather than "prevent using String". "Avoid" means that you should not do it. "Prevent" means you should make it so someone else cannot do it, or so it cannot happen by itself. Likewise, "avoid storing constant global variables in SRAM" (the constant part is important, you can't just move anything to PROGMEM!)
    – user20574
    Commented Jul 2, 2020 at 11:39
  • 1
    I've made a few more changes, in case you want to check them.
    – user20574
    Commented Jul 2, 2020 at 13:33
  • 1
    Arduino programs are called sketches, but I think program memory is still called program memory, not sketch memory. For example PROGMEM
    – user20574
    Commented Jul 6, 2020 at 12:28
7

I just want to add a single bullet to Michel Keijzers’ excellent answer:

  • think about every single item you are storing in memory and ask yourself the question: do I really need to keep this in RAM?

It may sound silly to state what many would consider obvious, but we have seen here many instances of novices who do not take this into consideration. As a simple example, consider this function that averages 500 analog readings:

int averageAnalogReading()
{
    // First take and store the readings.
    int readings[500];
    for (int i = 0; i < 500; i++)
        readings[i] = analogRead(inputPin);

    // Then compute the average.
    long sum = 0;
    for (int i = 0; i < 500; i++)
        sum += readings[i];
    return sum / 500;
}

Storing all those readings is completely useless, as you can just update the sum on the fly:

int averageAnalogReading()
{
    long sum = 0;
    for (int i = 0; i < 500; i++)
        sum += analogRead(inputPin);
    return sum / 500;
}

For the same reason, if you need some kind of running average to smooth data, you should consider using an exponentially-weighted running average, which can be incrementally updated without storing the readings.

3
  • Upvoted, very good point. The best way to improve is to check the algorithms itself. Short note: if you are interested in averages, also look at 'moving averages', also not costing additional SRAM for each measurement made. Commented Jul 2, 2020 at 8:21
  • 1
    @MichelKeijzers If you want a proper box-filter moving average then you do need to store the last N measurements. But if you can change it to an exponential filter, then you don't.
    – user20574
    Commented Jul 2, 2020 at 11:41
  • @user253751 You are right in that. Thanks for the clarification. Commented Jul 2, 2020 at 13:30
4

What are the generally practiced methods for the optimization program memory usage?

(nb. as per Edgar's comment I emphasise that this is about using PROGMEM more efficiently.)

  • If you can replace code with a table whose size is ≤ the lines of code, do it.

    • Instead of using a sequence of ifs, find a way to collapse the procedure into a table
    • Use tables of function pointers if it makes sense
    • Sometimes you can come up with a mini-language that is much more dense than AVR instructions, for example encoding robot logic into 16 commands, and then you can pack two commands per byte. This could collapse your memory usage 50-fold.
  • Use functions instead of repeated code—this may sound obvious but there are often subtle ways of rewriting code (but bear in mind that function calls have overhead)
  • Use hash tables rather than tables with big gaps
  • Use fixed point rather than floating point (e.g. you can take a byte and interpret its value as ranging from 0.00 to 2.55, instead of using a 4-byte float)

Is there any difference in memory usage if the variable is declared globally or locally.

Let's talk about the stack.

void A() {
    byte a[600];
    ...
}
void B() {
    byte b[400];
    ...
}
void loop() {
    byte xxx[1000];
    ...
}

This program will firstly use at least 1000 bytes of RAM all the time. There is no real difference compared to declaring xxx globally. But then what is critical is which function calls which.

If loop() calls A(), and then loop() calls B(), the program will not use more than 1600 at any time. However, if A() calls B(), or vice versa, the program will use 2000. To illustrate:

loop() [1000]
  └──── A() [1600]
  │    [1000]
  └──── B() [1400]
  └──── A() [1600]
  └──── B() [1400]

versus

loop() [1000]
  └──── A() [1600]
        └──── B() [2000]
  │    [1000]
  └──── A() [1600]
        └──── B() [2000]

Will it matter what the control statement/selection statements are (like if, switch )

Not much difference for a small number of cases. Otherwise it depends on your code. The best way is to just try both and see which is better. But:

switches usually use jump tables which are quite compact if you cover nearly every case in a range (0,1,2,3,4,..,100). ifs usually use a sequence of instructions, which take up more bytes and cycles than a jump table entry, but it makes more sense if you don't have a consecutive stretch of cases.

Usage of Serial monitor. Serial.print()

I don't believe that makes a lick of difference. Serial buffers are tiny (say 64 bytes, or 128 for a bigger board) and I believe they are allocated whether or not you use Serial.

Of course "literal strings like this" and char[] buffers consume memory. You can comment them out (or use #ifdefs) when you don't need them.

3
  • 2
    1. Note that your first 5 bullet points are about saving flash rather than saving RAM. The OP was not short on flash. The tables will actually cost RAM, unless you put them in PROGMEM. 2. Re “There is no real difference compared to declaring xxx globally”: that is, unless setup() is memory-hungry. 3. The serial buffers are not allocated if you don't use Serial. Commented Jul 2, 2020 at 7:37
  • Thanks @EdgarBonet 1. Yes, I was thinking of PROGMEM, sorry. 2. Could you explain this? Isn't setup()'s stack-allocated memory freed after it runs? 3. Thanks I have edited that.
    – Artelius
    Commented Jul 2, 2020 at 10:10
  • Re explain point 2: setup() and loop() do not usually run at the same time, so their stack usages do not add up. If you make xxx global, it will be allocated even while setup() is running. This should be no concern unless setup() is itself memory-hungry. Commented Jul 2, 2020 at 10:28
1

Since you asked for traditional ways, I am going to raise a traditional method. In this case, more than 50 years old.

Generate and analyze a listing.

Methodology:

  1. Compile all the code with debugging turned on (add -g).

  2. Link the code with debugging turned on, producing an ELF executable. Do NOT convert to an image loadable on the arduino.

  3. use objdump to make a listing. My usage of this read:

    ( avr-objdump --headers --source --disassemble --syms program.elf ; \
      avr-objdump --full-contents --section=.final_progmem program.elf ) > program.lst
    

    Your usage may vary based on what you prefer to see.

The point of this is that this allows you to see exactly what is using every byte of your memory.

You may want to play around with different optimizations, until you understand what you are seeing. -O0 produces the most comprehensible disassembly, while -Os makes the smallest.

One tip based on having done this: The Arduino libraries are designed for generality, not speed and memory efficiency.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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