By dave | September 20, 2018

Arduino Sketch compilation, cost of virtual tables, Wire memory use - part 2

In part 2 of this series we discuss how sketches compile on Arduino, along with the cost of using the virtual keyword to create virtual classes. Some things are not quite as clear cut as may be initially thought, especially in the very low memory environment of the ATMega328 (Arduino Uno). Lastly we discuss the memory usage of Wire and how to reduce it.

If you’ve not read static memory analysis for Arduino - part 1 then I recommend reading that first, as it sets the background for this article.


How Arduino sketches and libraries are compiled

In the Arduino environment, libraries are included in the sketch by including a header file from the library. When the library gets included, every source file in the library is compiled. Then, in order to create an embedded package of these files they are linked together.

In the linking phase, all of the compiled code is ’linked’, by this we mean that any external functions or variables defined will be matched from file to file. Normally, at this point unused globals are usually disposed of, but from what I’ve seen the AVR compiler for Arduino does not seem to do this. So this means every global variable in a library is included immediately, regardless of use.

Let’s take a look by generating a simple sketch that includes io-abstraction library. but does not use any of its capabilities. We’ll just include IoAbstraction.h, this does not include SwitchInput or TaskManager, but what we notice immediately is all the globals defined for both are immediately included.

Here’s the sketch:

#include <IoAbstraction.h>

void setup() {
}

void loop() {
}

And here’s an excerpt of the compiler output:

Compiling library "IoAbstraction"
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-g++" -c -g -Os -Wall -Wextra -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -MMD -flto -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10805 -DARDUINO_AVR_MEGA2560 -DARDUINO_ARCH_AVR   "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/variants/mega" "-I~/Documents/Arduino/libraries/IoAbstraction/src" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src" "~/Documents/Arduino/libraries/IoAbstraction/src/BasicIoAbstraction.cpp" -o "/tmp/arduino_build_588265/libraries/IoAbstraction/BasicIoAbstraction.cpp.o"
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-g++" -c -g -Os -Wall -Wextra -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -MMD -flto -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10805 -DARDUINO_AVR_MEGA2560 -DARDUINO_ARCH_AVR   "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/variants/mega" "-I~/Documents/Arduino/libraries/IoAbstraction/src" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src" "~/Documents/Arduino/libraries/IoAbstraction/src/EepromAbstraction.cpp" -o "/tmp/arduino_build_588265/libraries/IoAbstraction/EepromAbstraction.cpp.o"
... and more files
Compiling library "Wire"
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-g++" -c -g -Os -Wall -Wextra -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -MMD -flto -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10805 -DARDUINO_AVR_MEGA2560 -DARDUINO_ARCH_AVR   "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/variants/mega" "-I~/Documents/Arduino/libraries/IoAbstraction/src" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src" "~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src/Wire.cpp" -o "/tmp/arduino_build_588265/libraries/Wire/Wire.cpp.o"
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-gcc" -c -g -Os -Wall -Wextra -std=gnu11 -ffunction-sections -fdata-sections -MMD -flto -fno-fat-lto-objects -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10805 -DARDUINO_AVR_MEGA2560 -DARDUINO_ARCH_AVR   "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/variants/mega" "-I~/Documents/Arduino/libraries/IoAbstraction/src" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src" "~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src/utility/twi.c" -o "/tmp/arduino_build_588265/libraries/Wire/utility/twi.c.o"
Compiling core...
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-gcc" -c -g -x assembler-with-cpp -flto -MMD -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10805 -DARDUINO_AVR_MEGA2560 -DARDUINO_ARCH_AVR   "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/variants/mega" "~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino/wiring_pulse.S" -o "/tmp/arduino_build_588265/core/wiring_pulse.S.o"
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-gcc" -c -g -Os -Wall -Wextra -std=gnu11 -ffunction-sections -fdata-sections -MMD -flto -fno-fat-lto-objects -mmcu=atmega2560 -DF_CPU=16000000L -DARDUINO=10805 -DARDUINO_AVR_MEGA2560 -DARDUINO_ARCH_AVR   "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino" "-I~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/variants/mega" "~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/cores/arduino/WInterrupts.c" -o "/tmp/arduino_build_588265/core/WInterrupts.c.o"
... and more files
Linking everything together...
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-gcc" -Wall -Wextra -Os -g -flto -fuse-linker-plugin -Wl,--gc-sections,--relax -mmcu=atmega2560  -o "/tmp/arduino_build_588265/memTesting.ino.elf" "/tmp/arduino_build_588265/sketch/memTesting.ino.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/BasicIoAbstraction.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/EepromAbstraction.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/EepromAbstractionWire.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/IoAbstraction.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/IoAbstractionWire.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/SwitchInput.cpp.o" "/tmp/arduino_build_588265/libraries/IoAbstraction/TaskManager.cpp.o" "/tmp/arduino_build_588265/libraries/Wire/Wire.cpp.o" "/tmp/arduino_build_588265/libraries/Wire/utility/twi.c.o" "/tmp/arduino_build_588265/core/core.a" "-L/tmp/arduino_build_588265" -lm
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-objcopy" -O ihex -j .eeprom --set-section-flags=.eeprom=alloc,load --no-change-warnings --change-section-lma .eeprom=0  "/tmp/arduino_build_588265/memTesting.ino.elf" "/tmp/arduino_build_588265/memTesting.ino.eep"
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-objcopy" -O ihex -R .eeprom  "/tmp/arduino_build_588265/memTesting.ino.elf" "/tmp/arduino_build_588265/memTesting.ino.hex"
Global variables use 263 bytes (3%) of dynamic memory, leaving 7929 bytes for local variables. Maximum is 8192 bytes.

And here’s an excerpt of the map file that shows the biggest globals being brought in:

008002a6 l     O .bss	00000043 taskManager
00800223 l     O .bss	00000020 twi_txBuffer
0080026e l     O .bss	00000020 twi_masterBuffer
00800249 l     O .bss	00000020 twi_rxBuffer
008002e9 l     O .bss	0000001e switches

This is a pretty high level view, but designed to give just enough information to aid understanding. In short globals within a library are brought in immediately, any dependencies a library has must be there at compile time. Unfortunately, as of Sept, 2018 the Arduino library manager does not support dependencies so that is a manual process.

How the Arduino / AVR compiler deals with VTables (virtual functions)

When we use the virtual keyword on methods in a class, we are essentially late binding that method call, so that the implementation of the method is defined at runtime. This has a cost in the compiler, in that it has to create a virtual table per implementation class, one entry for each virtual method.

There is a lot of confusing information about this online, with different suggestions about how this memory is allocated. I’ll clear it up here with proof: Memory for all VTables is allocated in SRAM, yes that’s right, every virtual function will eat up some RAM.

Also see this article here that provides further evidence that this is correct: [https://www.avrfreaks.net/forum/unhappy-c-virtual-tables-sram]

My own analysis of the compiler output map (from part 1), showing that two virtual tables have been allocated in the data map (one for the base, one for the implementation). This comes out of RAM. However, it’s fairly small at 20 bytes.

                        RAM use  Which function
0080012a g     O .data	0000000a .hidden _ZTV13RotaryEncoder
00800120 g     O .data	0000000a .hidden _ZTV21HardwareRotaryEncoder

If we just think about this for a moment, we see why it’s this way. AVR is a Harvard architecture processor, as such if these structures were not stored in RAM, special handling in GCC would be needed whenever a virtual function was called. That’s because if the structure were in PROGMEM / FLASH special instructions would be needed to access it; therefore needing a significant change in the compiler core.

Understanding this doesn’t mean you shouldn’t use virtuals, as they don’t use that much memory. Just think if it’s really needed first or can be solved equally well another way.

The overhead of Wire in environments such as Arduino Uno (ATMEGA328)

Wire has a not unsubstantial overhead on Uno / ATMEGA328, as it uses four 32 byte buffers by default. This is all but irrelevant on larger boards such as Mega2560 or 32bit boards with more RAM.

Why are there 4 buffers in the first place? Three of the buffers are used by the underlying TWI functions that Wire uses (read buffer, write buffer and master buffer). These three get allocated the moment you include Wire.h, even if the Wire object is never used. On top of that there’s a buffer in the Wire object that’s only allocated if used.

Next, I’m going to show you how to reduce the buffer sizes, but before doing this you need to check your own code and any libraries you use to ensure the new buffer size will work properly, EG: nothing expecting a larger buffer in onReceive or when writing.

This should only be attempted by experienced programmers. Before doing any other step, back up the files you’re changing.

Changing the size of the Wire buffers

You’ll need to locate the directory where the Arduino tools have been installed. On MacOS I found Wire.h in the following location ~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src/Wire.h and I found twi.h in ~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire/src/utility/twi.h. They will be similarly placed on Windows or Linux.

The first step is to change twi.h. I usually halve the size to 16. We see the following define:

#ifndef TWI_BUFFER_LENGTH
#define TWI_BUFFER_LENGTH 32
#endif

We also need to change Wire.h buffer to the same size to avoid problems. In Wire.h change the following line:

#define BUFFER_LENGTH 32

To test this we again used the most basic sketch possible:

#include <Wire.h>

void setup() {
  Wire.begin();
}

void loop() {
  Wire.beginTransmission(0x20);
  Wire.write(0x00);
  Wire.endTransmission(0x20);
}

Let’s first compile the sketch without the memory changes:

Global variables use 187 bytes (2%) of dynamic memory, leaving 8005 bytes for local variables. Maximum is 8192 bytes.

Now let’s reduce the two buffer sizes to 16 and try again:

Global variables use 123 bytes (1%) of dynamic memory, leaving 8069 bytes for local variables. Maximum is 8192 bytes.

We can see an immediate 64 byte reduction in sketch size, and looking in the map we see the reduced buffers:

$ avr-objdump -x  memTesting.ino.elf | grep '\.bss' | grep 0010
00800212 l     O .bss	00000010 twi_txBuffer
00800254 l     O .bss	00000010 _ZN7TwoWire8txBufferE
0080023d l     O .bss	00000010 twi_masterBuffer
00800228 l     O .bss	00000010 twi_rxBuffer

Take note that if you alter TWI and Wire, these will need to be adjusted again after each update of the AVR tool chain.

Other pages within this category

comments powered by Disqus

This site uses cookies to analyse traffic, and to record consent. We also embed Twitter, Youtube and Disqus content on some pages, these companies have their own privacy policies.

Our privacy policy applies to all pages on our site

Should you need further guidance on how to proceed: External link for information about cookie management.

Send a message
X

Please use the forum for help with UI & libraries.

This message will be securely transmitted to our servers.