Vorsicht bei Compiler-Optimierungen

 

Dieses sehr kurze Tutorial möchte Ihnen eine gewisse Skepsis gegenüber Ihrem Compiler vermitteln. Gerade Anfänger und Anwender der Arduino-IDE (speziell mit älteren IDE <= 1.05) laufen sehr schnell in eine Optimierungsfalle, die einen schnell in den Wahnsinn treiben kann. 

 

Hinweis: Die konkreten Bespiele beziehen sich zwar auf den gcc- bzw. g++-Compiler, der z. B. im Hintergrund in der Arduino-IDE arbeitet, die grundsätzliche Problematik tritt allerdings bei allen Compilern und nicht nur im Mikrocontroller-Bereich auf.

 

Betrachten wir dazu das folgende Beispiel: 

Sie weisen z. B. im Setup-Teil Ihres Arduino-Sketches eine Interrupt-Service-Routine zu:

attachInterrupt(1, isrInt1, CHANGE);  
 

In dieser Interrupt-Service-Routine machen Sie nichts anderes, als ein Flag "isrFlag" zu setzen und eine Variable hoch zu zählen. 

 

isrInt1()

{

   isrFlag = true;

   iCount++;

}

 

In der Hauptschleife (Loop-Funktion) fragen Sie dann das isrFlag ab und nur, wenn es gesetzt ist, also ein Interrupt erfolgte, soll auch etwas passieren:

 

if (isrFlag == true)

{

   tueEtwas();

}

 

Im Setup-Teil haben Sie die Flag-Variable isrFlag zunächst auf 0 gesetzt. 

Die Logik des Programms lässt uns folgendes erwarten: 

Das Programm läuft in die loop-Schleife und prüft ständig, ob das Flag gesetzt ist. Erfolgt ein Interrupt, wird isrFlag auf "true" gesetzt und die Funktion "tueEtwas()" ausgeführt. 

 

Je nachdem, welche Optimierung Sie aber gewählt haben (und in der alten Arduino-IDE war das standardmäßig -Os und somit Optimierung auf Größe) ist das Ergebnis aber absolut unterschiedlich. Im vorliegenden Fall wird unsere Erwartung auch erfüllt und wir gehen davon aus. Würden Sie nun aber eine Schleife hinzufügen, die ähnlich wie diese aussähe ...

 

while(1) {

   if (isrFlag == true)

   {

      tueEtwas();

   }

   ...
  }
 
... würde überhaupt nichts mehr passieren. Warum? 
 
Der Compiler erkennt, das die Variable isrFlag am Anfang auf "false" gesetzt wird. Der Optimierer prüft nun, wo diese Variable auf "true" gesetzt wird. Er findet nur die Interrupt-Service-Routine, die er allerdings nicht als solche erkennt und findet ansonsten keinen Einsprung in diese Funktion. Also geht er davon aus, dass isrFlag nie gesetzt wird und optimiert die ganze if-Schleife weg. 
 
Das Verwirrende dabei ist aber, dass dieser Umstand auch schon ohne die while-Schleife gegeben ist. Und bei einigen Compilern schlägt die Optimierung auch dann schon zu. 
 
Bei der Fehlersuche gehen Sie logischerweise davon aus, dass das Programm auch das tut, was Sie programmiert haben. Das ist allerdings beim Einsatz eines Optimierers gerade nicht der Fall, denn er dient ja nunmal dazu, Ihr Programm zu verändern. 
 

Gerade "Einsteiger-Systeme" wie das Arduino-Konzept wollen es dem Nutzer ja möglichst leicht machen, aber trotzdem ist es wichtig, sich doch tiefer in die Materie einzuarbeiten, denn sonst ist man beim Auftreten von Fehlern oft ratlos. Die genannte Problematik tritt ja häufiger und nicht nur im Zusammenhang mit der Compiler-Optimierung auf. Allgemein ergibt sich das Problem, wenn eine Variable aus einem externen Kontext geändert wird, den der Compiler zum Zeitpunkt der Übersetzung nicht sehen kann (z. B. die Interrupt-Routine). Aus diesem Grund gibt es den Typ-Qualifizierer "volatile". Damit sagen wir dem Compiler: "Auch wenn es für Dich momentan nicht so aussieht, wir brauchen diese Variable auf jeden Fall, also lass die Finger davon!" Die Deklaration unseres "isrFlags" würde dann wie folgt aussehen:

volatile _Bool isrFlag;

Falls Sie die Compiler-Optimierungen aber ganz gezielt nur für bestimmte Funktionen ändern wollen, können Sie dies über die Funktionsattribute tun.
 
Einsatz der Funktionsattribute (function attributes)
 
Funktionsattribute geben Sie direkt hinter der Funktionsdeklaration im Programm selbst oder in der Headerdatei an. Würde sich unsere while-Schleife oben z. B. in der Funktion motorControl() befinden, dann sähe der Funktionsprototyp wie folgt aus:
 
void motorControl(int Speed, int Direction) __attribute__((optimize("O0")));
 
Jetzt wird der komplette Code z. B. mit der Optimierung "-Os" durchgeführt, nur die Funktion motorControl() wird ohne Optimierung übersetzt. 
 
Wenn Sie das Attribut nicht im Funktionsprototypen angeben wollen, können Sie dies auch direkt beim Aufruf tun, dann steht das Attribut vor der Funktion. Außerdem können Sie anstelle des Strings "O0" auch eine Zahl für den Optimierungslevel angeben:
 
void __attribute__((optimize(0))) motorControl(int Speed, int Direction)
{
   //code
}
 
Einsatz von #pragma gcc optimize
 
Seit der Version 4.4 gibt es beim gcc-Compiler ein spezielles Pragma zur Steuerung der Optimierung. Um sie damit auszuschalten, würde die Anweisung folgendermaßen aussehen:
 
#pragma GCC optimize ("O0")
func()
...
 
Alle Funktionen nach diesem Pragma werden jetzt ohne Optimierung übersetzt.
 
Um die Option nur gezielt für einen bestimmten Teil des Codes zu ändern, können Sie die beiden folgenden Varianten nutzen:
 
 
Version 1:
#pragma GCC push_options //rettet die bisherige Einstellung aller Optionen
#pragma GCC optimize (0)    //schaltet die Optimierung aus
func1();
func2();
#pragma GCC pop_options //stellt die vorherige Einstellung wieder ein
 
 
Version 2:
#pragma GCC optimize (0)
func1();
func2();
#pragma GCC reset_options //stellt die standardmäßigen Einstellungen wieder her

 

 
 

Copyright © Böcker Systemelektronik