Richtig optimieren

Bei der Überarbeitung der IRremote Bibliotheken sind mir ein paar überraschende Details aufgefallen, die ich gerne verbreiten möchte. Wer’s selbst ausprobieren möchte: einfach einen Sketch erstellen, der nur compilieren muß, und dann den Speicherbedarf der verschiedenen Varianten vergleichen. Ergebnisse mit Serial.write ausgeben, damit der Compiler nichts wegoptimieren kann! Hochladen und Zeitmessungen sind meist garnicht notwendig.

Die Wahl des Datentyps hat großen Einfluß auf Rechenzeit und Speicherbedarf. Am effizientesten sind byte oder uint8_t, solange der Wertebereich ausreicht. Dagegen hat sich bool und enum als Bremse herausgestellt, bei bool wohl wegen der Tests auf die zulässigen Werte true (1) und false (0).

Für enums werden standardmäßig 16 Bit (int) verwendet, die auf einem 8-Bitter natürlich langsam werden. Das ist besonders unangenehm, wenn man die Zustände eines Automaten einfach als enum angeben möchte, statt jeden einzeln per #define oder const :frowning:
Kennt jemand eine Compiler-Option zum Einstellen der enum Größe?

Bei lokalen Variablen scheint der Compiler manche int zu byte zu verkürzen, keine Ahnung wie der das erkennt, und wie zuverlässig diese Optimierung ist.

Bei Vergleichen scheinen (x) und (!x) gleich schnell zu sein wie (x==0) oder (x==1) oder (x!=0), anscheinend wird stets mit einer Konstanten verglichen.

Bei float Variablen wird’s etwas undurchsichtig (Compilerfehler?).

float f;
f = 1.5;

ist harmlos, da werden nur 4 Bytes kopiert. Aber bei Rechnungen wird’s seltsam. Natürlich werden da Unterprogramme für die Arithmetik mit eingebunden, nur die Größe im Programmspeicher ist inkonsistent. Fügt man f *= 2; oder f += f; hinzu, die eigentlich das gleiche machen, verbraucht += mehr Speicher als *=, am wenigsten wird aber verbraucht wenn beide Operationen gleichzeitig im Programm stehen! Wie das?

Ändert man dann die Konstante bei *= in einen anderen Wert, dann werden zwei Unterprogramme (für * und +) eingefügt.

Bei Zwischenrechnungen mit Fließpunktzahlen, die mit ganzen Zahlen gefüttert und nachher ganzzahlig weiterverwendet werden, kann der Compiler ggf. alle Fließpunktrechnungen wegoptimieren. Das ist sehr angenehm beim Vergleich von Meßwerten mit einer Toleranz (in Prozent), wie in ir_NEC.cpp mit MATCH_MARK und MATCH_SPACE demonstriert :slight_smile:

Modulo scheint der Compiler bei int nicht ordentlich zu optimieren, was bei i%4 durchaus möglich wäre. Hier die Programmgrößen für verschiedene Versionen (int/byte):
I = ++i % 4; // 958/862 - byte wird anscheinend optimiert
i = (i+1)%4; // 958/868 - huch?
if (++i > 3) i=0; // 886/870
i++; if (i>3) i=0; // 886/870
if (i<3) ++i; else i=0; // 886/870
if (i<3) i=i+1; else i=0; // 886/870
i = (i<3) ? i+1 : 0; // 882/868
i = (i+1) & 3; // 874/862 - geht leider nur bei Zweierpotenzen

DrDiettrich: Bei Vergleichen scheinen (x) und (!x) gleich schnell zu sein wie (x==0) oder (x==1) oder (x!=0), anscheinend wird stets mit einer Konstanten verglichen.

Hier muss man auf Assembler-Ebene denken. In Assembler gibt es eine einfachen Weg um auf 0 abzufragen. Man kopiert den Wert in den Akkumulator. Dadurch wird das Zero Flag geändert. Wenn es gesetzt ist ist der Wert 0. Dann kann man einfach "jump if (not) zero" machen. Das ist schneller als die "Vergleiche und springe wenn gleich/ungleich" Befehle.

Ich glaube mich an Code zu erinnern wo das nicht optimiert wurde. Hatte mich gewundert.

Aus dem gleich Grund können for-Schleifen die gegen 0 zählen eventuell schneller sein. Ausprobiert habe ich es aber noch nicht.

Für enums werden standardmäßig 16 Bit (int) verwendet, die auf einem 8-Bitter natürlich langsam werden. Das ist besonders unangenehm, wenn man die Zustände eines Automaten einfach als enum angeben möchte, statt jeden einzeln per #define oder const :-( Kennt jemand eine Compiler-Option zum Einstellen der enum Größe?

In GCC gibt es glaube ich einen Compiler-Schalter. Aber besser man verwendet einfach C++11 :)

C++11 hat sogenannte "strongly typed enums" eingeführt die typsicher sind (also nicht in Integer gewandelt werden können) und deren Werte keinen globalen Scope haben (d.h. zwei verschiedene enums können die gleichen Bezeichner enthalten): https://en.wikipedia.org/wiki/C%2B%2B11#Strongly_typed_enumerations

Außerdem kann man den Datentyp angeben. Und das kann man auch mit klassischen C enums machen:

enum Enum1 : byte {Val1, Val2};

Um das klarzustellen: du musst nicht gleich "enum class" verwenden. Die Angabe des Datentyps geht auch mit C enums!

Fügt man f *= 2; oder f += f; hinzu, die eigentlich das gleiche machen, verbraucht += mehr Speicher als *=

-->Ich nehme stark an, dass er die Addition an dieser Stelle als Addition ausführt, wie man sie aus dem Schulbuch kennt, während die Multiplikation als einfacher Linksshift interpretiert und ausgeführt wird. Probehalber kannst Du ja die Addition 2x ausführen und die Multiplikation nicht mit 2, sondern mit 3 ausführen.

Serenifly: Hier muss man auf Assembler-Ebene denken. In Assembler gibt es eine einfachen Weg um auf 0 abzufragen. Man kopiert den Wert in den Akkumulator. Dadurch wird das Zero Flag geändert.

Du hast Dich wohl im Prozessor vergriffen? :-(

Aus dem gleich Grund können for-Schleifen die gegen 0 zählen eventuell schneller sein. Ausprobiert habe ich es aber noch nicht.

C++11 hat sogenannte "strongly typed enums" eingeführt die typsicher sind (also nicht in Integer gewandelt werden können) und deren Werte keinen globalen Scope haben (d.h. zwei verschiedene enums können die gleichen Bezeichner enthalten):

Mit derlei Optimierungen und Typsicherheit bin ich von Delphi sehr verwöhnt. Bei Arduinos zwingt mich die IDE zu C/C++, und das Atmel Studio mit Ada steht noch auf meiner ToDo Liste (ziemlich weit unten).

DerLehmi: -->Ich nehme stark an, dass er die Addition an dieser Stelle als Addition ausführt, wie man sie aus dem Schulbuch kennt, während die Multiplikation als einfacher Linksshift interpretiert und ausgeführt wird.

Es geht um float, da wird beim Multiplizieren mit Zweierpotenzen nichts geshiftet.

Es geht um float, da wird beim Multiplizieren mit Zweierpotenzen nichts geshiftet.

-->Hupsa....tut mir Leid, hab ich überlesen :blush:

DrDiettrich: Du hast Dich wohl im Prozessor vergriffen?

Der AVR hat zwar kein extra Akkumulator Register, aber das Prinzip ist identisch. Jedes der 32 General Purpose Register hat so Zugriff auf die ALU. Also jedesmal wenn ein Wert in eines der rXX Register kopiert wird werden die Flags gesetzt

Es geht um float, da wird beim Multiplizieren mit Zweierpotenzen nichts geshiftet.

FYI: Multiplizieren mit ZweierPotenzen geht auch bei float super einfach und ohne Rundungsfehler. ( exp++ )

Serenifly: Der AVR hat zwar kein extra Akkumulator Register, aber das Prinzip ist identisch. Jedes der 32 General Purpose Register hat so Zugriff auf die ALU. Also jedesmal wenn ein Wert in eines der rXX Register kopiert wird werden die Flags gesetzt

In der Instruction Set Summary steht das aber ganz anders!?

ARG! :o

Hatte wirklich gedacht gelesen zu haben, dass MOV die Flags beeinflusst. Sorry. Da gibt es aber einen einfachen Trick. Man addiert 0 zu einem Register (oder macht eine andere Operation die den Wert nicht ändert, z.B. OR oder ORI). Das dauert auch nur einen Takt. Und dann werden die Flags gesetzt. Wobei es auch TST genau dafür gibt was ein Und mit sich selbst macht.

Ich war aber gedanklich wirklich nicht in AVR Assembler. :s

Kommt natürlich auch darauf an was man überhaupt macht. Vielleicht hat man vorher schon eine Operation gemacht die Flags angefasst hat. z.B. eben dekrementieren mit DEC. Und genau da kann man dann nämlich eventuell Takte sparen, weil man nicht immer diese Operation machen muss.