Das Titelbild dieses Beitrags basiert auf einem Bild von TheDigitalArtist auf Pixabay.

Link-Zeit-Optimierung (LTO) ist eine sehr leistungsfähige Compiler-Optimierungstechnik. Wie ich feststellen musste, ist LTO nicht verträglich mit dem Debuggen objektorientierter Programme unter GCC, zumindest für AVR-MCUs. Ich bemerkte das im Zusammenhang mit dem Debuggen eines Arduino-Programms und es dauerte eine ganze Weile, bis feststellte, dass LTO hier der Schuldige ist.

LTO ist eine Compiler-Optimierungstechnik, die das gesamte Programm berücksichtigt, bevor das Linken der Objektdateien die endgültige Binärdatei erzeugt. Es ist eine neuere Technik, die erst seit etwa zehn Jahren Teil der GCC-Toolchain ist. Wer sich für die Entwicklung dieser Technik interessiert, sollte sich den interessanten Blogbeitrag von Honza Hubička über die Geschichte der LTO anschauen.

Im Gegensatz zu lokaleren Techniken, die nur eine Kompilierungseinheit (d. h. eine Quelldatei) berücksichtigen, kann LTO nicht verwendete Funktionen, Methoden und Klassen-Member in allen kompilierten Einheiten, die miteinander verknüpft sind, eliminieren. Einer der Nachteile ist, dass solche Optimierungen sehr lange dauern können. Anstatt also nur Adressen während der Verknüpfungsphase aufzulösen, was wenige Sekunden dauert, kann die globale Link-Zeit-Optimierung Minuten dauern. Darüber hinaus kann es aufgrund des aggressiven Inlining von Funktionen über Kompilierungseinheiten hinweg zu größeren Stack-Frames kommen, was zu Stack-Überläufen führen kann. Schließlich kann das Debuggen aufgrund des Löschens von Teilen der Codes und des Neuanordnens anderer Teile schwieriger werden. Tatsächlich erwähnt Chris Coleman das LTO-Optimierungs-Flag als eines der Flags, die man im Kontext der Embedded-Programmierung möglichst nicht verwenden sollte, und empfiehlt, es nur als letzten Ausweg zu verwenden, wenn man verzweifelt Codespace in einem Embedded-Projekt sparen muss.

Debuggen von OO-Programmen

LTO ist seit Arduino IDE 1.8.11 (veröffentlicht Anfang 2020) aktiviert, da es in der Regel zu deutlich geringeren Anforderungen an Flash-Speicher führt. Und ich muss sagen, dass ich es überhaupt nicht bemerkt habe, was ein gutes Zeichen ist. Bis gestern jedenfalls.

Ich hatte ein kleines objektorientiertes Programm geschrieben, das als Testfall für den Debugger gedacht ist, an dem ich gerade arbeite. Hier kommt eine vereinfachte Version davon.

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() { }

Wir haben also eine Basisklasse TwoDObject und eine abgeleitete Klasse Rectangle mit einigen Instanzvariablen und -funktionen und einer minimalen Vererbung. Nichts wirklich Besonderes.

Nun wollen wir dieses Programm entweder mit der Arduino-IDE oder arduino-cli kompilieren und die Optimierungsstufe so einstellen, dass sie Debugging-freundlich ist (siehe meinen früheren Beitrag über die Konfiguration der Arduino-IDE zum Debuggen), d.h. man gibt die Optimierungsoption -Og an. Nachdem der Debugger gestartet und die Binärdatei hochgeladen wurde, wollen wir jetzt wissen, was der Typ der Instanz r ist (mit dem Debugger-Befehl ptype) und was der Wert von r.x ist.

...
(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}

Die Instanz ist also vom Typ struct Rectangle, und die geerbte Membervariable r.x existiert nicht? Sehr lustig!

Fügen wir nun die Option -fno-lto zu den build-flags hinzu, d.h. die LTO-Optimierung wird deaktiviert. Wenn man arduino-cli verwendet, kann man das tun, indem man die Option --build-property hinzufügt:

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

Ein erneuter Aufruf des Debuggers führt nun zu einem „realistischeren“ Bild:

...
(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}

Das sieht so aus, wie man es von vornherein erwartet hätte. D.h. man sollte beim Debuggen flto-Option die deaktivieren, speziell, wenn man OO-Programme debuggen möchte.

Interessanterweise scheint dieses Problem auf die avr-gcc-Toolchain beschränkt zu sein. Es passiert nicht mit nativen GCC unter Ubuntu.

Views: 15