AVR-GCC-Codeoptimierung: Unterschied zwischen den Versionen
(Struktur) |
|||
Zeile 148: | Zeile 148: | ||
Generell gilt für all diese Optionen, daß sie abhängig vom Projekt zu einer Codeverbesserung oder -verschlechterung führen können — dies ist i.d.R. vom Projektcode abhängig. | Generell gilt für all diese Optionen, daß sie abhängig vom Projekt zu einer Codeverbesserung oder -verschlechterung führen können — dies ist i.d.R. vom Projektcode abhängig. | ||
=== | === Änderung des Binärinterfaces per Option === | ||
-funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums | ; Warnung: Folgende Optionen ändern das Binärinterface (ABI) und sind daher mit Vorsichtzu geniessen! Wird eine Anwendung mit diesen Schaltern übersetzt, dann ist sicher zu stellen, daß ''alle'' Module inclusive Libraries damit derzeugt werden die ABI-Änderung sich nicht auf Code in Bibliotheken auswirkt! | ||
Vorsicht in auch deshalb geboten, weil manche Entwicklungsumgebungen wie "Atmel Studio" das ABI ''per Default'' verändern und ohne daß der Anwender es extra anfordert. | |||
;<tt>-funsigned-char</tt>: Anders als im avr-gcc ABI ist der Typ <tt>char</tt> unsigned anstatt signed. | |||
:<u>Besser</u>: Verwende die C99-Typen wie <tt>uint8_t</tt> aus dem C99-Header <tt>stdint.h</tt>. | |||
;<tt>-funsigned-bitfields</tt>: Bitfelder mit Basetype <tt>char</tt>, <tt>short</tt>, <tt>int</tt> <tt>long</tt> und <tt>long long</tt> werden als unsigned implementiert anstatt als signed wie in der avr-gcc ABI. | |||
:<u>Besser</u>: Wenn ein Bitfeld unsigned sein soll, dann mach es unsigned! | |||
;<tt>-fpack-struct</tt>: Im Gegensatz zur avr-gcc ABI werden Strukturen und Unions per defulat gepackt. | |||
:<u>Besser</u>: Wenn ein zusammengesetzter Typ gepackt werden soll, mache ein Typedef mit explizitem <tt>__attribute__((packed))</tt>! | |||
;<tt>-fshort-enums</tt>: Enum-Typen werden so kurz wie möglich implementiert anstatt als <tt>int</tt> gemäß avr-gcc ABI. | |||
== Anpassungen der Quelle == | == Anpassungen der Quelle == |
Version vom 23. Juli 2012, 07:25 Uhr
Entstanden aus diesem Thread sollen hier ein paar Hinweise/Erfahrungen gegeben werden, um den Quellcode in Punkto Größe und Geschwindigkeit zu optimieren. En detail ist das Thema komplex, da es stark von der Codeoptimierung des Compilers abhängt. Es ist im Einzelfall ratsam zu prüfen, ob die eigenen Maßnahmen auch erfolgreich waren. Die Diskussionen [1] bzw. [2] können als Anhaltspunkte dienen, wie eine solche Prüfung ablaufen kann.
Prinzipien der Optimierung
Wie so oft sollte man nicht einfach wild drauf los optimieren und sich zunächst ein paar Dinge klar machen.
- Warum will ich optimieren?
- Was kann man sinnvoll optimieren?
- Wieviel Rechenzeit oder Speicher soll dabei gespart werden?
- Wie kann optimiert werden?
- "Verfrühte Optimierung ist die Wurzel allen Übels"
Viele Optimierungen sind "Angst-Optimierungen", die nicht wirklich nötig sind. Die Gefahr mit Optimierungen ist, den Code tot zu optimieren, sprich Lesbarkeit, Portierbarkeit und ggf. Fehlerfreiheit sinken massgeblich. Kurz und knapp in diesem BLOG formuliert.
Warum
Optimieren sollte man nur, wenn
- der Speicher nicht mehr ausreicht (RAM, Flash)
- Die Laufzeit für bestimmte Programmteile zu groß wird und somit bestimmte (Echtzeit-)Ausgaben nicht im erforderlichen Zeitrahmen erledigt werden
Weiter sollte man folgende Punkte gegeneinander abwägen:
- Codeverbrauch
- Datenverbrauch. Statisch/Stack/Heap
- Mittlere Laufzeit/maximale Laufzeit
- Entwicklungszeit
- Portabilität (Compiler, Hardware, ...)
- Verständlichkeit der Quelle, siehe Strukturierte Programmierung auf Mikrocontrollern
- ABI-Konformität
Was
Die goldene Regle lautet: 90% der Rechenleistung werden in 10% des Codes verbraucht. Diese 10% muss man finden und zum richtigen Zeitpunkt optimieren. Der Rest muss nur sauber und lesbar geschrieben sein. Was jedoch nichts bringt, ist eine Funktion, die von 1 Minute Programmlaufzeit lediglich 1 Sekunde verbraucht, um den Faktor 10 schneller zu machen. Die Programmlaufzeit sinkt dann von 60 Sekunden auf 59.1 Sekunden. Der Aufwand, die Funktion um einen Faktor 10 schneller zu machen ist aber meistens beträchtlich! Kann ich aber den Code, der für die 59 Sekunden verantwortlich ist um einen Faktor 10 schneller machen, dann sinkt die Gesamtlaufzeit von 60 Sekunden auf 6.9 Sekunden. Dort bringt Optimieren augenscheinlich viel mehr!
Um die optimierungswürdigen Stellen zu finden, muss man sein Programm analysieren. Dazu gibt es verschiedene Möglichkeiten.
Speicherverbrauch nach Funktionen aufschlüsseln
- map-File
- dort sind alle globalen und statischen Variablen enthalten. Eine Map-Datei kann mit den GNU-Tools während des Linkens angelegt werden:
> avr-gcc ... -Wl,-Map,foo.map
- Die Option -Wl bewirkt, daß avr-gcc die angehängen Optionen unverändert an den Linker weiterreicht. Dieser erzeugt dann das Mapfile "foo.map", eine Textdatei.
- avr-size
- Mit Tools wie avr-size kann die Platzbelegung einzelner Module ermittelt werden:
> avr-size -x foo1.o foo2.o ...
- bzw. die Platzbelegung der elf-Datei:
> avr-size foo.elf
- avr-nm
-
> avr-nm --size-sort -S foo.elf
- ergibt eine Liste mit der Größe aller Objekte: der erste Spalte enthälte die Adresse, die zweite Spalte die Größe, die dritte den Typ und die vierte Spalte den zugehörigen Symbolnamen. Der Typ ergibt sich aus der folgenden Zuordnung, wobei Großbuchstaben globale Symbole kennzeichnen und Kleinbuchstaben Symbole, die Modul-lokal sind:
- T/t
- Objekte in der text-Section: Funktionen, Daten im Flash
- D/d
- Objekte im data-Segment (initialisierte Daten)
- B/b
- Objekte im bss-Segment (Null-initialisierte Daten)
- avr-gcc
- Der Compiler hat bereits Informationen über die übersetzten Funktionen, die man direkt zur Analyse verwenden kann. Dazu lässt man avr-gcc die Assembler-Ausgabe, die ohne weiteres Zutun nur als temporäre Datei angelegt wird, abspeichern. Etwa für die Quelldatei foo.c:
> avr-gcc -save-temps foo.c -c ...
- Die Assembler-Datei wird damit als foo.s angelegt und nicht gelöscht. (Das ebenfalls angelegte Präcompilat foo.i wird nicht benötigt). Für jede Funktion gibt avr-gcc 3.4.x im Prolog einen Kommentar der Form[1]
/* prologue: frame size=0 */
- aus, was die Größe des aktuellen Frames angibt. Dies ist der Platz auf dem Stack, der für lokale Variablen benötigt wird. Am besten ist es, wenn die Frame-Size wie im Beispiel gleich 0 ist. Ansonsten sollte man versuchen, diese Größe auf Null zu bringen. Für Variablen, die nicht in Registern gehalten werden können, müssen Speicherzugriffe in den Stack erzeugt werden. Diese machen das Programm sowohl größer aus auch langsamer. Zudem reserviert avr-gcc bei solche Funktionen das Y-Register als Frame-Pointer; das Y-Register steht damit nicht mehr für lokale Variablen zur Verfügung was sich ebenfalls ungünstig auf die Codegüte auswirkt. Ein Grund für das Anlegen eines Frames können zu viele lokale Variablen sein (zB lokale Puffer/Arrays) oder lokale Variablen/Strukturen/Parameter mit ungünstigen Größen, etwa eine 3-Byte große Struktur.
- Neben dieser Information gibt avr-gcc Kommentare der Gestalt
/* prologue end (size=2) */
- aus die darüber informieren, wie viele Register auf dem Stack gesichert wurden.
- Zusammen mit Werkzeugen wie grep, die in jedem Linux und jeder WinAVR-Distribution enthalten sind, findet man schnell Übeltäter wie Funktionen mit Frame.
- Assembler-Code sichten
- Ein kurzer Blick auf den erzeugten Assembler-Code zeigt oft, wie gut der Compiler den Code umgesetzt hat. Den erzeugten Assembler-Code zu überfliegen ist wesentlich zeitsparender als selbst in Assembler zu programmieren. Je nach Gusto verwendet man zur Einsicht den Assembler-Code, den avr-gcc ausgibt (s.o.), Assembler-Dumps des Assemblers, List-Files oder HEX-Dumps. Siehe auch[2]
- Hilfsmittel
- einkaufen oder selber bauen. Es gilt herauszufinden, welche Funktion massig Stack durch lokale Variablen verbraucht. Stacktracer können das. Wenn man keinen hat, dann muss man sich eben selber einen bauen, indem man den Stackpointer mitloggt. Zur Not einen Code-Review machen: Alle Funktionen optisch durchgehen und die identifizieren, die viele Variablen anlegen. Dann die Aufrufhierarchie der Funktion feststellen: Wirken sich die vielen Variablen überhaupt aus oder entsteht mein Problem durch eine tiefe Funktionsaufrufhierarchie, bei der zwar wenige Variablen pro Funktion im Spiel sind, aber die Menge der ineinandergeschachtelten Aufrufe 'das Kraut fett macht'
- Profitools
- können das alles fast auf Knopfdruck, kosten aber viel Geld
Laufzeit messen
- Simulator
- In Echtzeit mittels Testpin, welche an Anfang einer Funktion/Blocks gesetzt wird und am Ende wieder gelöscht wird. Mit einem Oszilloskop kann man so sehr einfach die Laufzeit messen.
- Anmerkung
- Solche Messverfahren liefern immer nur eine untere Schranke für die Laufzeit, niemals eine obere Schranke. Eine obere Schranke, wie man sie etwa in sicherheitsktitischen Systemen benötigt, liefert eine statische Codeanalyse.
Wieviel
Der Aufwand von Optimierungen wächst exponentiell. Die letzten paar Prozent brauchen überproportional viel Aufwand.
Wie
Meist muss man die Wahl treffen ob man Speicher oder Rechenzeit sparen will, beides gleichzeitg geht meist nicht. Das Konzept heißt 'Space for Time' und kann in beide Richtungen verwendet werden. Als Beispiel soll eine komplizierte Berechnung dienen. Diese kann man relativ kompakt in eine Funktion packen, welche dann aber eher langsam ist. Oder man benutzt eine sehr große Tabelle, in welcher die Ergebnisse schon für jeden Eingangswert vorausberechnet wurden. Diese Lösung ist sehr schnell, verbraucht aber sehr viel Speicher.
- Inlining von Funktionen erhöht den Speicherverbrauch, senkt aber die Laufzeit. Beispiel: Funktion A ist 50 Byte groß und wird 10 mal im Programm aufgerufen. Ein Aufruf kostet 10 Byte:
- Ohne Inline: 10 * 10Byte + 50 Byte = 150 Byte Platzverbrauch
- Mit Inline: 10 * 50 Byte = 500 Byte
- Optimierer einschalten
- möglichst keine Floating Point Operationen, besser ist meist Festkommaarithmetik
- Formeln umstellen und zusammenfassen
- Variablen so klein wie möglich, uint8_t wo's nur geht.
- Wirklich zeitkritische Funktionen und Interrupts als Assemblercode in separater Datei
GCC-Optionen
Optimierungs-Level
avr-gcc kennt mehrere Optimierungsstufen:
- -O0
- Keine Optimierung des erzeugten Codes. Diese Optimierungsstufe optimiert den Resourcenverbrauch des Hostrechners und die Nachvollziehbarkeit der erzeugten Codes anhand von Debug-Information. Alle lokalen Variablen werden auf dem Stack angelegt und nicht in Registern gehalten. Es werden keine komplexen Optimierungsalgorithmen angewandt; lediglich Konstanten wie 1+2 werden zu 3 gefaltet.
- -O1
- Je höher die Optimierungsstufe, desto schwieriger ist der erzeugte Code nachvollziehbar — auch mit Debugger. Diese O-Stufe ist ein Kompromiss zwischen aggressiver Optimierung und Nachvollziehbarkeit des erzeugten Codes. Ein ehernes Gesetz in GCC ist, dass er den gleichen Code erzeugen muss unabhängig davon, ob Debug-Information erzeugt wird oder nicht. Im Umkehrschluss erlaubt volle Debug-Unterstützung nicht alle Optimierungen, wozu diese Optimierungsstufe dient.
- -O2
- Optimierung auf Geschwindigkeit. Für AVR nur mässig sinnvoll, da sich der Codezuwachs nicht in einen entsprechenden Geschwindigkeitszuwachs transformiert. Dies liegt vor allem daran, daß Sprünge und Funktionsaufrufe auf AVR im Vergleich zu anderen Architekturen sehr billig sind. Es bringt also kaum einen Geschwindigkeitszuwachs, einen Block zu kopieren um einen Sprung zu sparen. Hingegen vergrößert dies den Code deutlich.
- -O3
- Ditto. Auf Teufel-komm-raus Funktionen zu inlinen, Schleifen aufzurollen oder gar Funktionen mehrfach für unterschiedliche Aufruf-Szenarien zu implementieren, ist auf einem kleinen µC wie AVR der Overkill.
- -Os
- Optimierung auf Codegröße. Die bevorzugte Optimierungsstufe für AVR und viele andere µC.
Jede O-Option ist ein Sammlung von verschiedenen Schaltern, welche bestimmte Optimierungsstrategien aktivieren. Um zu sehen, welche Schalter dies genau sind, erzeugt man wie oben beschrieben mit den Schalten
-save-temps -fverbose-asm
die Assembler-Ausgabe von gcc und schaut die Optionen im s-File nach. Einzelne Optionen lassen sich gezielt aktivieren bzw. deaktivieren und damit zum Beispiel zum -Os-Paket hinzufügen.
Eine Ausnahme bildet -O0: Hier ist Code-Optimierung generell deaktiviert, und Optimierungsschalter bleiben ohne Wirkung.
Feinabstimmung der Optimizer
Kandidaten für Optimierungsoptionen sind folgende Schalter. -m kennzeichnet maschinenspezifische Schalter, die nur für AVR gültig sind. -f bzw. -fno- sind maschinenunabhängige Schalter, die auch für andere Architekturen verfügbar sind.
- -fno-split-wide-types
- Je nach Quelle kann die Deaktivierung von -fsplit-wide-types besseren Code ergeben.
- -fno-inline-small-functions
- Relativ kleine Funktionen /immer/ zu inlinen kann den Code unnötig vergrößern, dieser Schalter unterbindet das automatische Inlinen kleiner Funktionen.
- -finline-limit=wert
- Maximalwert für automatisch geinlinte Funktionen. In einschlägigen Foren werden kleine Werte für wert vorgeschlagen, z.B. 1…3
- -mcall-prologues
- Die für aufwändige Funktionen mitunter recht langen push/pop-Sequenzen werden durch Hilfsfunktionen ersetzt. Das kann vor allem bei grossen Programmen Platz sparen. Die Ausführungszeit steigt an.
- -fno-jump-tables
- Switch-Statements werden hierdurch mitunter deutlich kürzer.
- -fno-move-loop-invariants
-fno-tree-loop-optimize - Einige Schleifenoptimierungen, welche die Registerlast erhöhen und für AVR kaum zu einem Geschwindigkeitszuwachs führen, werden deaktiviert.
- -fno-tree-switch-conversion
- Neue GCC-Versionen können switch/case Anweisungen u.U. in Loopup-Tabellen umwandeln, die im RAM abgelegt werden, siehe auch PR49857. Dieser Optimierung ist bei RAM-Knappheit in Betracht zu ziehen, bring aber natürlich nur dann etwas, wenn diese Optimierung auch ausgeführt wurde.
- -fno-optimize-sibling-calls
- Tailcall-Optimierung kann den Code vergrößern, wenn Epiloge mehrfach erzeugt werden. In diesem Fall deaktiviert man die Tailcall-Optimierung. Wirksam ab Version 4.7.
- -maccumulate-args
- Ab 4.7: Funktionen, die mehrere printf-artige Aufrufe enthalten und viele Artumente per Stack an diese übergeben, werden u.U kleiner.
- -mstrict-X
- Ab 4.7: Beeinflusst die Art und Weise, wie das X-Register zur Adressierung verwendet wird.
- -mbranch-cost=wert
- Setzt die Kosten, mit der der Compiler einen bedingten Sprunge veranschlagt. Default-Wert ist wert=0.
- -fno-caller-saves
- Kann zu effizienteren Pro-/Epilogen beitragen.
- -fno-tree-ter
- Es gibt Fälle, in denen der Compiler die Berechnung temporärer Variablen über volatile-Zugriffe und Memory-Barriers zieht, siehe PR53033. Von der C-Spezifikation her ist dies zulässig, kann aber zu unerwünschter Umsortierung der volatile-Operation führe, z.B. wenn es sich dabei um eine SEI-Instruktion handelt. Ohne diese Optimierung wird der Code evtl. etwas größer, aber Probleme wie im PR beschrieben können vermieden werden.
- --param case-values-threshold=wert
- Ab 4.7: Schwellwert an Einträgen in einem switch/case, ab dem anstatt eines binären if/else-Entscheidungsbaums eine Sprungtabelle zu den case-Labels erzeugt wird. Voreinstellung ab 4.7 ist wert=7. Ältere Compilerversionen verwenden andere Werte. Siehe auch -f[no-]jump-tables.
Generell gilt für all diese Optionen, daß sie abhängig vom Projekt zu einer Codeverbesserung oder -verschlechterung führen können — dies ist i.d.R. vom Projektcode abhängig.
Änderung des Binärinterfaces per Option
- Warnung
- Folgende Optionen ändern das Binärinterface (ABI) und sind daher mit Vorsichtzu geniessen! Wird eine Anwendung mit diesen Schaltern übersetzt, dann ist sicher zu stellen, daß alle Module inclusive Libraries damit derzeugt werden die ABI-Änderung sich nicht auf Code in Bibliotheken auswirkt!
Vorsicht in auch deshalb geboten, weil manche Entwicklungsumgebungen wie "Atmel Studio" das ABI per Default verändern und ohne daß der Anwender es extra anfordert.
- -funsigned-char
- Anders als im avr-gcc ABI ist der Typ char unsigned anstatt signed.
- Besser: Verwende die C99-Typen wie uint8_t aus dem C99-Header stdint.h.
- -funsigned-bitfields
- Bitfelder mit Basetype char, short, int long und long long werden als unsigned implementiert anstatt als signed wie in der avr-gcc ABI.
- Besser: Wenn ein Bitfeld unsigned sein soll, dann mach es unsigned!
- -fpack-struct
- Im Gegensatz zur avr-gcc ABI werden Strukturen und Unions per defulat gepackt.
- Besser: Wenn ein zusammengesetzter Typ gepackt werden soll, mache ein Typedef mit explizitem __attribute__((packed))!
- -fshort-enums
- Enum-Typen werden so kurz wie möglich implementiert anstatt als int gemäß avr-gcc ABI.
Anpassungen der Quelle
Attribute noreturn, OS_main und OS_task
Mikrocontroller-Programme laufen normalerweise in einer Endlosschleife, so dass die main-Routine nie verlassen wird. Teilt man dies dem Compiler mit, kann er bestimmte Optimierungen durchführen. So ist es zum Beispiel unnötig, Code zum Sichern und Zurücklesen von Registern zu erzeugen.
Das Mitteilen funktioniert beim gcc über attribute, die man der Deklaration oder bei der Implementierung einer Funktion anhängt: <c>static void main_loop (void) __attribute__((noreturn)); void main_loop (void) {
for(;;) { // Hauptschleife }
}</c> oder <c>static void __attribute__((noreturn)) main_loop (void) {
for(;;) { // Hauptschleife }
}</c>
Die Funktion main_loop kann dann in main aufgerufen werden:
<c>int main() {
main_loop();
return 0;
}</c> Das abschließende return wird vom Compiler wegoptimiert und belegt keinen Speicher.
avr-gcc kennt weiterhin die Attribute OS_main und OS_task, die leider nicht dokumentiert sind (Stand 07/2011). Die Verwendung von OS_main kann etwa aussehen wie folgt. Natürlich kann auch wie oben die Hauptschleife in einer eigenen Funktion implementiert werden, und das return verursacht keinen zusätzlichen Code: <c>int __attribute__((OS_main)) main(void) {
for(;;) { // Hauptschleife }
return 0;
}</c>
Statische (globale) Variablen in einer Struktur sammeln
Das erleichtert dem Compiler die Adressierung, da er den Basiszeiger wiederverwenden kann. Die Codegröße kann dann noch von der Reihenfolge der struct-Member abhängen. Die häufigst benutzte Variable sollte am Anfang stehen, dann kann sie ohne Offset direkt mit dem Basiszeiger adressiert werden. Ansonsten in Gruppen, wie die Variablen auch gebraucht werden. Hier kann man viel rumprobieren.
Beispiel: <c> typedef struct {
uint16_t sec; // Meistbenutze Variable an den Anfang uint16_t minute; uint16_t hour;
} time_t;
time_t global; // Globale Struktur definieren uint8_t min; // Als Vergleich: einzelne globale Variable
int main(void) {
time_t *time = &global; // Zeiger auf die globale Struktur // LDI R30,LOW(global) ; Init Z pointer // LDI R31,(global >> 8) ; Init Z high byte if (++time->sec == 60) { // LDD R16,Z+2 ; Load with displacement // INC R16 ; Increment // STD Z+2,R16 ; Store with displacement // CPI R16,LOW(60) ; Compare // BRNE ?0005 ; Branch if not equal } if ( ++min == 60) { // LDS R16,LWRD(min) ; Load direct from SRAM // INC R16 ; Increment // STS LWRD(min),R16 ; Store direct to SRAM // CPI R16,LOW(60) ; Compare // BRNE ?0005 ; Branch if not equal } return 0;
} </c>
Dadurch, dass die Strukturvariable über LDD/STD (LDD/STD 2 Bytes; LDS/STS 4 Bytes) angesprochen werden kann, werden an dieser Stelle 4 Bytes eingespart. Hinzu kommen jedoch noch einmal die 4 Bytes für die Initialisierung des Z-pointers, sodass die Einsparung erst bei mehreren Globalvariablen zum Tragen kommt.
- Anmerkung
- Dieses Beispiel zeigt sehr schön, daß solcherlei "Optimierung" ohne Wissen um die Arbeitsweise des eingesetzten Compilers nach hinten losgehen können oder ins Leere laufen. Der erzeugte Code (avr-gcc 4.3.3 -Os) ist:
main: /* prologue: function */ lds r24,global lds r25,(global)+1 adiw r24,1 sts (global)+1,r25 sts global,r24 lds r24,min subi r24,lo8(-(1)) sts min,r24 ldi r24,lo8(0) ldi r25,hi8(0) /* epilogue start */ ret
- D.h. es wird nicht indirekt auf die Daten zugegriffen. Grund ist, daß gcc die Adresse zur Compilzeit ermitteln kann und dieses Wissen ausnutzt. Angemerkt sei noch, daß der Code im Beispiel von oben entweder gefaket ist und nicht von einem Compiler stammt (die wahrscheinlichere Variante), oder der Compiler inkorrekten Code erzeugte: Das INC erhöht nur die unteren 8 Bit der Komponenten, welche jedoch 16-Bit Werte sind.
- Dennoch ist die angedeutete Zusammenfassung von inhaltlich zusammengehörenden Variablen sinnvoll und besser als ein Schwarm frei-flottierender int-Variablen.
Multiplikationen mit Konstanten
Der Compiler instanziiert sofort eine teure allgemeine Bibliotheksfunktion, auch wenn es anders ginge. Ich hatte eine einzige 32-bit Multiplikation mit 10 drin, die mir ein mulsi3 beschert hat. Mit a = (b<<3) + (b<<1) geht es in dem Fall kürzer. Wie gesagt, map-File beobachten.
- Anmerkung
- Variablen als unsigned definieren, dann sollte der Compiler das selbst machen.
- Anmerkung
- Auch Schieben ist teuer auf AVR. Schauen wir uns also mal an, was aus folgendem Code wird:
<c> uint32_t foo (uint32_t i) {
return i*10;
}
uint32_t bar (uint32_t i) {
return (i << 1) + (i << 3);
} </c>
00000032 <foo>: 32: 2a e0 ldi r18, 0x0A ; 10 34: 30 e0 ldi r19, 0x00 ; 0 36: 40 e0 ldi r20, 0x00 ; 0 38: 50 e0 ldi r21, 0x00 ; 0 3a: 19 d0 rcall .+50 ; 0x6e <__mulsi3> 3c: 08 95 ret 0000003e <bar>: 3e: 26 2f mov r18, r22 40: 37 2f mov r19, r23 42: 48 2f mov r20, r24 44: 59 2f mov r21, r25 46: 22 0f add r18, r18 48: 33 1f adc r19, r19 4a: 44 1f adc r20, r20 4c: 55 1f adc r21, r21 4e: e3 e0 ldi r30, 0x03 ; 3 50: 66 0f add r22, r22 52: 77 1f adc r23, r23 54: 88 1f adc r24, r24 56: 99 1f adc r25, r25 58: ea 95 dec r30 5a: d1 f7 brne .-12 ; 0x50 <__SREG__+0x11> 5c: 26 0f add r18, r22 5e: 37 1f adc r19, r23 60: 48 1f adc r20, r24 62: 59 1f adc r21, r25 64: 95 2f mov r25, r21 66: 84 2f mov r24, r20 68: 73 2f mov r23, r19 6a: 62 2f mov r22, r18 6c: 08 95 ret 0000006e <__mulsi3>: 6e: ff 27 eor r31, r31 70: ee 27 eor r30, r30 72: bb 27 eor r27, r27 74: aa 27 eor r26, r26 00000076 <__mulsi3_loop>: 76: 60 ff sbrs r22, 0 78: 04 c0 rjmp .+8 ; 0x82 <__mulsi3_skip1> 7a: a2 0f add r26, r18 7c: b3 1f adc r27, r19 7e: e4 1f adc r30, r20 80: f5 1f adc r31, r21 00000082 <__mulsi3_skip1>: 82: 22 0f add r18, r18 84: 33 1f adc r19, r19 86: 44 1f adc r20, r20 88: 55 1f adc r21, r21 8a: 96 95 lsr r25 8c: 87 95 ror r24 8e: 77 95 ror r23 90: 67 95 ror r22 92: 89 f7 brne .-30 ; 0x76 <__mulsi3_loop> 94: 00 97 sbiw r24, 0x00 ; 0 96: 76 07 cpc r23, r22 98: 71 f7 brne .-36 ; 0x76 <__mulsi3_loop> 0000009a <__mulsi3_exit>: 9a: 9f 2f mov r25, r31 9c: 8e 2f mov r24, r30 9e: 7b 2f mov r23, r27 a0: 6a 2f mov r22, r26 a2: 08 95 ret
- Der Funktionsaufruf samt Lib-Funktion ist garnicht sooo teuer. Bereits mit zwei Multiplikationen im Programm — auch einer Multiplikation mit einer anderen Konstanten oder einer Variablen — gewinnt die lib-Version, da der Code wiederverwendet wird. Übrigens sind diese Multiplikationsroutinen und auch die in die libgcc enthaltenen Divisionen keine "normalen" Funktionen wie sie von C erzeugt werden. avr-gcc weiß genau, welche Register diese Routinen belegen und welche nicht. Damit ist der Aufruf einer solchen Funktion billiger als ein herkömmlicher Funktionsaufruf, bei dem die Funktion als Blackbox behandelt werden muss, die alle call-clobbered Register zerstört.
Alle Variablen nur so breit wie nötig
Hatte ich eigentlich schon, nur an einigen wenigen Stellen war ich da etwas nachlässig. Mitunter reicht ein kleinerer Typ doch, wenn man z. B. vorher geeignet skaliert. Am besten nur die skalaren Typen aus <stdint.h> verwenden, das erleichtert auch das Folgende. Bei RAM Knappheit: kann ich Strings sinnvollerweise aus dem RAM ins Flash verbannen? Kann ich es mir leisten mehrere Flag-Variablen in ein Byte zusammenzufassen, auch wenn dann die Zugriffe möglicherweise etwas langsamer werden.
Logische Operatoren werden auf int-Größe erweitert
Obwohl der AVR ein 8-Bit Controller ist, weitet der avr-gcc an manchen Stellen Vergleiche von zwei 8-Bit Variablen auf 16-Bit auf. Als Beispiel sei dabei folgendes gezeigt:
<c> void foo (uint8_t a, uint8_t b) {
if (a == ~b) { // clr r19 ; clear register // mov r24,r22 ; copy register // clr r25 ; clear register // com r24 ; one's complement // com r25 ; one's complement // cp r18,r24 ; compare registers // cpc r19,r25 ; compare registers with carry // brne .L1 ; branch if not equal }
} </c> Den zweiten Vergleich mit der Negation weitet der Compiler auf 16 Bit auf. Ein Cast verhindert dieses: <c> void foo(uint8_t a, uint8_t b) {
if (a == (uint8_t) ~b) { // com r22 ; one's complement // cp r25,r22 ; compare registers // brne .L1 ; branch if not equal }
} </c> Die Einsparung an Speicher zwischen den beiden Versionen beträgt 12 Bytes. Außerdem ist die zweite Version um 6 Takte schneller.
- Achtung
- Tatsächlich handelt es sich dabei nicht um ein Optimierungsproblem, sondern einen typischen Programmierfehler. Die beiden Varianten sind keineswegs identisch. Bei Variablen vom Typ uint8_t wird der Ausdruck (a == ~b) immer falsch sein: a=0x0000...0x00ff, ~b=0xff00...0xffff.
Compileroption -mint8 für 8-Bit Arithmetik als Default
Mit obigen casts überall sähe der Code ziemlich schlimm aus. Blöd auch, wenn man mal einen Type ändert, dann muß man sorgsam nach den zugehörigen casts suchen. Mit dem Compilerschalter -mint8 wird das zum Standard. Bei mir hat das etwa 200 Byte gespart! Man sollte dafür aber keine ints mehr im Code haben, nur noch Typen definierter Größe aus <stdint.h>. Literal-Werte muß man ggf. anpassen (z. B. mit postfix L long machen) damit sie nicht überlaufen, Compiler-Warnings beachten. Ist anscheinend noch etwas experimentell(?), mit dem aktuellen gcc 4.1.1 gibt es eine Unverträglichkeit in <stdint.h>, der kriegt ein Problem mit den 64-bit Typen. Ist aber wohl in Arbeit, ich habe einen Patch gesehen.
- Warnung
- Diese Option verändert das Binärinterface! Funktionen, die nicht mit dieser Option übersetzt wurden, sind nicht unbedingt kompatiablen mit solchen, die mit dem Schalter erzeugt wurden. Da die Bibliotheken — auch die Compiler-interne libgcc — ohne diesen Schalter generiert werden, ist mit Problemen zu rechnen. Weiterhin sind bestimmte Typen nicht mehr verfügbar bzw. werden mit anderer Semantik belegt, etwa int und long. Für die Option gibt es in avr-gcc 4.x kein Support mehr.
Stack auf 256 Bytes begrenzen
Mit dem Compileflag -mtiny-stack wird für den Stack eine einfachere Adressierung möglich, die aber "nur" 256 Byte Stacktiefe erlaubt. Wenn man nicht exzessiv automatische Variablen benutzt (Arrays!) oder eine hohe Verschachtelungstiefe hat, sollte das ausreichen. Hat mir nochmal knapp 100 Byte (!) kleineren Code erzeugt.
Speichern von globalen Flags
Oft werden in den Programmen Flags verwendet um beispielsweise eingetroffene Interrupts in der main-Routine auszuwerten. Hierzu wird üblicherweise eine globale Variable verwendet.
Um den Wert dieser Variable abzufragen, muss sie jedoch erst aus dem SRAM in ein Register geladen werden, und kann dann erst auf ihren Status hin überprüft werden. Eine Möglichkeit ist, der globalen Variablen ein einziges Register fest zuzuordnen: <c> register uint8_t counter8_1 asm("r2"); register uint8_t counter8_2 asm("r3"); register uint16_t counter16_1 asm("r4"); // r4:r5 register uint16_t counter16_2 asm("r6"); // r6:r7 </c> siehe auch: http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_regbind
Als Alternative kann man ein nicht verwendetes Register des I/O-Bereichs verwenden. Dabei würde sich z. B. das Register eines zweiten UARTs, oder das EEPROM-Register anbieten, falls diese nicht benötigt werden.
Neuere AVR-Modelle besitzen für diesen Zweck 3 frei verwendbare Bytes im bitadressierbaren I/O-Bereich: GPIOR0-2.
- Warnung
- Dieses Vorgehen verändert das ABI! Um dieses Feature fehlerfrei anzuwenden, ist einiges an Wissen über die Interna von GCC notwendig. Auch ein korrekt funktionierendes Programm ist keine Garantie dafür, daß die globalen Register fehlerfrei implementiert wurden. Unter Umständen bringen erst spätere Codeänderungen/-erweiterung den Fehler zum Vorschein, und weil der Fehler vorher nicht akut war, sucht man sich den Wolf an der falschen Stelle im Code anstatt bei der globalen Registern. Siehe auch Globale Register.
Puffern von volatile-Variablen
Der Compiler behandelt volatile-Variablen bei mehreren Manipulationen wie heiße Kartoffeln. Für jeden einzelnen Vorgang wiederholt sich das Spiel:
- aus dem Speicher holen
- bearbeiten
- zurückspeichern
Unter Umständen ist dieses Verhalten unsinnig. Ein Minimalbeispiel:
<c> volatile char var;
ISR() {
var++;
if (var > 100) var = 0;
}
void main (void) {
while (1) printf (var);
} </c>
Hier wird var pro ISR-Ausführung zwei mal aus dem RAM geholt und zurückgeschrieben. Das ist überflüssig, weil die Interruptrountine nicht unterbrochen werden kann. Aus Sicht der ISR bräuchte man eigentlich kein volatile, kann es aber wegen des Zugriffs aus main heraus nicht weglassen. Eine Lösung findet sich im folgenden Schnipsel:
<c> volatile char var;
ISR() {
char temp = var;
if (++temp > 100) temp=0;
var = temp;
}
void main (void) {
while (1) printf (var);
} </c>
Hier wird die globale Variable var in der lokalen Variable temp gepuffert. Ein Nachteil durch das Anlegen von temp ergibt sich nicht, da das dafür verwendete Register für die Manipulation sowieso benötigt wird.
Wie alle Optimierungen kann dieses Vorgehen auch nach hinten losgehen: Wenn Laden und Zurückspeichern von var weit auseinanderliegen (extrem lange ISR), müllt man sich die Register zu. Im schlimmsten Fall wird temp sogar zwischenzeitlich auf dem Stack ausgelagert.
Schleifen
Bei Schleifen, die eine bestimmte Anzahl an Durchläufen ausgeführt werden sollen, ist es besser den Schleifenzähler vorher auf einen Wert zu setzen, und am Ende einer Do-While Schleife diesen zu dekrementieren. So beschränkt sich die Sprungbedingung auf ein brne (branch if not equal).
Beispiel: <c> uint8_t counter; counter = 100; do {
// mach irgendetwas
} while (--counter); </c>
Unbenutzte Funktionen und/oder Variablen entfernen
F: Mir ist aufgefallen, dass der Linker nicht benutzte Funktionen trotzdem mit linkt und Speicherplatz belegt. Gibt es eine Möglichkeit diese Funktionen automatisch weg zu lassen?
A: Dem GNU Linker sagt man mit --gc-sections, dass er unbenutzte Sektionen rauswirft. Mit --print-gc-sections listet er die rausgeworfenen auch auf. Dem GCC kann man mit -ffunction-sections sagen, dass er jede Funktion in eine eigene Sektion legt, damit funktioniert das auch unterhalb der Ebene einer Quellcodedatei (also eine Funktion rausschmeissen obwohl fünf andere in derselben Datei gebraucht werden). Mit der Option -fdata-sections geht das auch für statische Variablen (Forumsbeitrag von Andreas B.).
Vorsicht: Je nach Implementierung der Interruptsprung- bzw. Vektorleiste kann es dazu führen, dass alle eigenen Interrupt-Handler ebenfalls wegoptimiert werden. Dies passiert dann, wenn es im Code keinen Verweis (typisch: Ermittlung der Adresse zum Eintrag in eine Interrupt-Vektortabelle oder in Hardwareregister eines Interrupt-Controllers) auf die Handler-Funktion gibt oder die Funktion, in der der Verweis auf eine ISR enthalten ist, nie aufgerufen wird. In solchen Fällen kann es notwendig sein, die Handler mit __attribute__((used)) zu versehen. Bei Verwendung der Makros aus der avr-libc (in WinAVR enthalten, z.B. ISR()) ist dies nicht erforderlich, da das Attribut bereits in den Makro-Definitionen enthalten ist (avr-libc/interrupt.h/ __INTR_ATTRS). In manch anderer Umgebung, wie bei einigen Quellcodes für ARM-basierte Controller, ist das Attribut jedoch zu ergänzen.
Optimierung der Ausführungsgeschwindigkeit
Hierzu gibt es schon eine Application-Note von Atmel. Diese AppNote bezieht sich auf den IAR-Compiler. Die darin genannten "Optimierungen" sind für avr-gcc größtenteils obsolet oder bleiben bestenfalls ohne Effekt.
Weblinks:
- AVR035: Efficient C Coding for AVR
- Program optimization auf Wikipedia, engl.
- Sensor smoothing and optimised maths on the Arduino - Multiplikation vs. Division
Fußnoten
- ↑ Für avr-gcc 4.x sehen die Kommentare anders aus oder fehlen je nach Compilerversion ganz
- ↑ roboternetz.de: Assembler-Dump erstellen mit avr-gcc
Links
Atmel AVR4027: Tips and Tricks to Optimize Your C Code for 8-bit AVR Microcontrollers (Beispiel-Code)