7

When compiling a sketch, after linking, the build outputs a kind of forecast on RAM usage, e.g.:

Minimum Memory Usage: 1456 bytes (71% of a 2048 byte maximum)

Is it possible to somehow get a breakdown or itemization of these 1456 bytes and details on exactly where in my code, or in included libraries, they are used?

4 Answers 4

10

Since you seem interested about RAM, rather than flash usage, I suggest you look at the symbol table of your compiled program, which will be more telling than the disassembly. On my Linux system, I usually do the following:

avr-nm -Crtd --size-sort the_program.elf | grep -i ' [dbv] '

avr-nm is the utility for displaying the symbol table. It is normally packaged with the Arduino IDE. It is invoked here with the following options:

  • --size-sort is quite explicit
  • -C means “demangle the C++ names”
  • -r means “reverse sort”, i.e. sort from largest to smallest
  • -td means “display numbers in decimal rather than hexadecimal”

This will output the list of all symbols that have a size, in three columns: size, type and name. You are only interested in the symbols that consume RAM, i.e. those of type b (BSS, or uninitialized data), d (initialized data) and v (vtable), either in upper or lower case. The grep command is a standard Unix utility which is used here to extract only the relevant lines from the output of avr-nm.

Example output for a small program:

00000068 B tx_buffer
00000068 B rx_buffer
00000034 B Serial
00000016 V vtable for HardwareSerial
00000004 B timer0_overflow_count
00000004 B timer0_millis
00000002 b loop::last_print
00000001 b timer0_fract

The first 4 entries come from the Serial object. The 3 entries starting with timer0_ are used by the Arduino timekeeping functions (millis(), micros() and delay()). The entry named loop::last_print is a static local variable I declared in loop().

4
  • That looks promising, but I will have to wait until tonight to try it. Will it only list the 'local' name of the variables? I suppose that e.g. the tx_buffer and rx_buffer variables listed in your example are actually HardwareSerial::_tx_buffer and HardwareSerial::_rx_buffer? Might omitting the -C option give any hint on the variable's class name?
    – jarnbjo
    Nov 14, 2016 at 10:23
  • @jarnbjo: You get the full name of the variable, e.g. loop::last_print is a local variable from loop() named last_print. The serial buffers are actually outside HardwareSerial in my version of the Arduino core, and HardwareSerial has no static variables, so you see no HardwareSerial::foo. The per-instance variables are part of the sole instance Serial and are not listed separately. Omitting -C will only hurt readability, e.g. it will print _ZZ4loopE10last_print instead of loop::last_print. Nov 14, 2016 at 11:13
  • I see. The fields in the HardwareSerial class were the closest match I could find for tx_buffer and rx_buffer in the current version of the library source code and I didn't consider that you might have an older library version. That makes sense.
    – jarnbjo
    Nov 14, 2016 at 13:41
  • This solved my problem. I had expected the compiler optimizer to be much more clever than it obviously is and I had lots of dependencies linked into the binary, which are never used. Without much more than deleting unused library files saved me almost 500 bytes of RAM usage.
    – jarnbjo
    Nov 14, 2016 at 23:56
2

Looking at an assembly listing of your code is the most direct method for finding out details of space allocation. On my Ubuntu Linux system, I use a shell script (that is, a file containing shell commands) like the following to produce an assembly listing of the most-recently-compiled .ino file, which must be in the working directory when the script executes:

#!/bin/sh
# Look for .ino in current directory, and find recent /tmp/build...
item=*.ino
[ -z "$item" ] && echo ino file not found && exit
BDIR=/tmp/$(ls -t /tmp | egrep -m1 build.*tmp)
BASE=$(basename $item .ino)
avr-objdump -S -I$PWD $BDIR/$BASE.cpp.elf > $BASE.ino.asm

All the stuff before the avr-objdump line allows the script to find out the name of the current .ino file and the name of the /tmp/ subdirectory where the .ino's .elf file is found.

You can also use avr-objdump to get a more concise size listing, as follows, where $BDIR and $BASE are as in the script above and represent the build directory in /tmp/, and the basename of the .ino file.

avr-objdump -C -d $BDIR/$BASE.cpp.o| egrep -C2 '^Disassembly' | egrep -v '^--|^$|^Disassembly' | less

This will produce a list with two-line entries like the following, in which each line beginning with 00000000 is a subroutine name, and the next line shows the last line of its compiled code. In this example output, the setup_PowerDown() routine has 0xA4 bytes of code, ISR __vector_13 has 0xC6 bytes, keepAlive() has 0x10 bytes, etc.

00000000 <setup_PowerDown()>:
  a4:   08 95           ret
00000000 <__vector_13>:
  c6:   18 95           reti
00000000 <keepAlive()>:
  10:   08 95           ret
00000000 <eeWrite1(unsigned char, unsigned char)>:
  3a:   08 95           ret
00000000 <eeWrite2(unsigned char, unsigned char)>:
  18:   0c 94 00 00     jmp     0       ; 0x0 <eeWrite2(unsigned char, unsigned char)>
00000000 <parValidate(unsigned char, unsigned char, unsigned char)>:
  64:   08 95           ret
00000000 <loadSettings()>:
  e0:   08 95           ret
00000000 <applySettings()>:
  84:   08 95           ret

Some IDEs provide symbol table information that includes sizes of routines, stack frames, and data. I don't know if that's available via Arduino IDE, AVR-libc, or GNU Binutils. The symbol table provided by avr-readelf -s (as in following command) provides quite detailed symbol information that seems quite useless in its raw form.

avr-readelf  -s  $BDIR/$BASE.cpp.o

The methods described above are somewhat ad hoc, and depend on files left after avr-gcc or avr-g++ run. I don't know of any “official” methods for getting the information you want to see.

2

You can automatically generate a "map" file every time you compile. This gives you a full breakdown of how everything is linked together by the linker, including the size of each section and other useful debugging information.

A simple modification to platforms.txt in hardware/arduino/avr is all that is needed. Find the line starting compiler.c.elf.flags= and add to the end of it:

 -Wl,-Map,"{build.path}/{build.project_name}.map"

If you like you can replace {build.path} with a hard-coded path, such as your desktop, and the .map with .txt to allow it to easily be opened by notepad or whatever you use (assuming Windows, of course).

1
  • Also very helpful, but do you happen to know why the avr-nm tool suggested by Edgar in his answer lists memory usage, which is not accounted for in this generated map. avr-nm e.g. lists 64 bytes used by an rxBuffer (full name _ZL8rxBuffer), which is not listed in the map, and which I am not able to find in any dependency.
    – jarnbjo
    Nov 15, 2016 at 0:04
1

This was a great question and helped me recently as I'm building quite an extensive Arduino app using a Nano (ATMega328 and 32K of program memory). At one point I was almost out of program memory and I was a bit shocked.
I didn't think the code was that large.

My Tests and Results

With the comments I read through the posts on this answer I decided to build a blank app and examine the results as I added libraries etc. Here are my results (Using Arduino IDE 1.8.13):

  1. 🚩 blank sketch takes up 444 bytes of program storage space (max is 30720 bytes).

  2. Sketch which includes 6 libraries✔ uses up 6218 bytes

    <LiquidCrystal_I2C.h>, <Wire.h>,<ds3231.h>,<SD.h>,<EEPROM.h>,<SoftwareSerial.h>

  3. adding one line Serial.begin(9600); uses 6324 bytes NOTE: 6324 - 6218 = 106 bytes even though I only added 19 bytes (the characters for that line).

  4. Adding on more line Serial.print("a"); results in 6342 bytes used. That's 18 more bytes (6324 - 6342 = 18) which is the exact number of characters I added with that line (including semicolon).

  5. Adding a 2nd line with Serial.print("a"); results in 6358 (16 bytes)

  6. Adding a 3rd line of Serial.print("a"); results in 6374 (16 additional bytes).🏁

I hope this helps a bit. I'm going back to my app now and removing all the Serial lines I used for debug output. They're just eating up program memory now. Actually, I'll probably even use the conditional compilation (shown in this SO answer) so I can still print when in debug.

🚩 Blank sketch means one with only the void setup() {} void loop() {} structures and nothing else.

✔ These are the libraries which I am using in my project.

🏁 I came back and added these additional lines with the amount of Program Memory used because there was a comment that lines of code do not impact memory at all. Of course they do. At some point all Program Memory is used up and your program will no longer fit into flash. This is what I was having a challenge with as my extensive program (which includes sensors, LCD, sd card, and bluetooth) continues to grow.

12
  • The length of a line of source is completely unrelated to memory usage. Jun 2, 2021 at 18:10
  • @DaveNewton Yeah, I should've thought about that since it must be counting the compiled bytes, right? is it the bytes added to .elf, maybe?
    – raddevus
    Jun 2, 2021 at 18:15
  • Also, I'm not sure it's completely unrelated, because more source code means larger program and larger binary size in the end. Otherwise why would you reach a limit as your program grows and why would the IDE warn you about the size of your app all the time?
    – raddevus
    Jun 2, 2021 at 18:21
  • What I said was that the length of a line of source is unrelated to memory usage--for example, if the exposed instance of the serial driver was ThisIsTheRealSerialDriverInstanceInUse.print("a"); the compiled size would be (assuming no debug symbols) the same as S.print("a"). Obviously a larger program is... larger--but there's a difference between the size of source code in bytes (the human readable form) and the size of the executable. Jun 2, 2021 at 18:49
  • In other words, counting the characters in a line of source, while it may coincidentally align with memory usage, it's just that--a coincidence. Jun 2, 2021 at 18:53

Your Answer

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

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