The featured image of this post is based on a picture by TheDigitalArtist on Pixabay.

Link-time optimization (LTO) is a very powerful compiler-optimization technique. As I noticed, it does not go very well together with debugging object-oriented programs under GCC, at least for AVR MCUs. I noticed that in the context of debugging an Arduino program and it took me quite a while to figure out that LTO is the culprit.

Link-time optimization

LTO is a compiler optimization technique that takes into account the entire program right before linking produces the final binary. It is a more recent technique that is part of the GCC toolchain only since roughly ten years ago. If you are interested in the development of this technique, there is an interesting blog post by Honza Hubička on the history of LTO.

In contrast to more local techniques that take only one compilation unit (i.e., source file) into account, LTO can eliminate unused functions, methods, and class members across all the compilation units that are linked together. One of the drawbacks is that such optimizations can take very long. So instead of just resolving addresses during the linking phase, which takes a few seconds, the global link-time optimization may take minutes. In addition, because of aggressive inlining of functions across compilation units, it may lead to larger stack frames, which can lead to stack overflows. Finally, because of deleting parts of the codes and rearranging other parts, debugging may become harder. In fact, Chris Coleman mentions the LTO optimization flag as one of the worst flags to use in the context of embedded programming, and recommends to use it only “as a last resort optimization if you are desperate to save codespace in an embedded project”.

Debugging OO programs

LTO is enabled since Ardunio IDE 1.8.11 (released beginning of 2020) because it usually results in significantly lower requirements on flash memory. And I must say that I did not notice it at all, which is a good sign. Until yesterday, that is.

I had written a small object-oriented program, which is intended to be a test case for the debugger I am working on. Here comes a simplified version of it.

class TwoDObject {
public:
  int x, y;
  TwoDObject(int xini, int yini) {
    x = xini; y = yini;
  }
  void move(int xchange, int ychange) {
    x = x + xchange; y = y + ychange;
  }
};

class Rectangle : public TwoDObject {
public:
  int height, width;
  Rectangle(int xini, int yini, int heightini, int widthini) : 
     TwoDObject(xini, yini) {
        height = heightini; width = widthini;
  }
  int area(void) {
    return (width*height);
  }
};

Rectangle r {10, 11, 5, 8};

void setup(void) {
  Serial.begin(9600);
  Serial.print(F("r position: ")); Serial.print(r.x); 
  Serial.print(","); Serial.println(y);
  Serial.print(F("  area: ")); Serial.print(r.area()); 
  Serial.println();

  Serial.println(F("Move r by +10, +10:"));
  r.move(10,10);
  Serial.print(F("r position: ")); Serial.print(r.x); 
  Serial.print(","); Serial.println(y);
}

void loop() { }

So, we have a base class TwoDObject and a derived class Rectangle with a few member variables and functions and a minimal amount of inheritance going on. Nothing really fancy.

Let us now compile this sketch using either the Arduino IDE or arduino-cli and let us set the optimization level to be debugging-friendly (see my earlier post on enabling the Arduino IDE for debugging), i.e., let us specify the optimization option -Og. After having started the debugger and uploaded the binary to the Arduino board, we may want to know what the type of the instance variable r is (using the debugger command ptype) and what the value of the member variable r.x is.

...
(gdb) pytpe r
type = struct Rectangle {
    int height;
    int width;
  public:
    void __base_ctor(int, int, int, int);
    int area(void);
}
(gdb) print r.x
There is no member or method named x.
(gdb) print r
$1 = {height = 1000, width = 0}

So, the instance variable is of type struct Rectangle, and the inherited member variable r.x does not exist? Very funny, indeed.

Let us now add the option -fno-lto to the build-flags, i.e., effectively disabling the LTO optimization. When using arduino-cli, one can do that by adding the --build-property option:

arduino-cli compile -b ... -e  --build-property "build.extra_flags=-Og -fno-lto"

Invoking the debugger again leads now to a more “realistic” picture:

...
(gdb) ptype r
type = class Rectangle : public TwoDObject {
  public:
    int height;
    int width;

    Rectangle(int, int, int, int);
    int area(void);
}
(gdb) print r.x
$1 = 0
(gdb) print r
$2 = {<TwoDObject> = {x = 0, y = 12544}, height = 59, width = 0}

This looks how one would have expected it in the first place. So, the take home message here is that one should disable the flto option when debugging. In particular when one wants to debug OO programs.

Interestingly, this problem seems to be restricted to the avr-gcc toolchain. It does not happen with native gcc under Ubuntu.