|
|
AVR-GCC-CodeoptimierungEntstanden 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. [Bearbeiten] Prinzipien der OptimierungWie so oft sollte man nicht einfach wild drauf los optimieren und sich zunächst ein paar Dinge klar machen.
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. [Bearbeiten] WarumOptimieren sollte man nur, wenn
Weiter sollte man folgende Punkte gegeneinander abwägen:
[Bearbeiten] WasDie 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. [Bearbeiten] Speicherverbrauch nach Funktionen aufschlüsseln
[Bearbeiten] Laufzeit messen
[Bearbeiten] WievielDer Aufwand von Optimierungen wächst exponentiell. Die letzten paar Prozent brauchen überproportional viel Aufwand. [Bearbeiten] WieMeist 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.
[Bearbeiten] Optimierung der Größe[Bearbeiten] GCC-interne Optimierungavr-gcc kennt mehrere Optimierungsstufen:
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 -fsave-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. -O0 optimiert auf Resourcenverbrauch des Compilers und auf Nachvollziehbarkeit per Debug-Info (so diese erzeugt wird). Kandidaten dafür 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.
Generall 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. [Bearbeiten] Attribute noreturn, OS_main und OS_taskMikrocontroller-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:
oder
Die Funktion main_loop kann dann in main aufgerufen werden:
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:
[Bearbeiten] Statische (globale) Variablen in einer Struktur sammelnDas 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:
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.
[Bearbeiten] Multiplikationen mit KonstantenDer 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.
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
[Bearbeiten] Alle Variablen nur so breit wie nötigHatte 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. [Bearbeiten] Logische Operatoren werden auf int-Größe erweitertObwohl 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:
Den zweiten Vergleich mit der Negation weitet der Compiler auf 16 Bit auf. Ein Cast verhindert dieses:
Die Einsparung an Speicher zwischen den beiden Versionen beträgt 12 Bytes. Außerdem ist die zweite Version um 6 Takte schneller.
[Bearbeiten] Compileroption -mint8 für 8-Bit Arithmetik als DefaultMit 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.
[Bearbeiten] Stack auf 256 Bytes begrenzenMit 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. [Bearbeiten] Speichern von globalen FlagsOft 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:
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.
[Bearbeiten] Puffern von volatile-VariablenDer Compiler behandelt volatile-Variablen bei mehreren Manipulationen wie heiße Kartoffeln. Für jeden einzelnen Vorgang wiederholt sich das Spiel:
Unter Umständen ist dieses Verhalten unsinnig. Ein Minimalbeispiel:
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:
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. [Bearbeiten] SchleifenBei 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:
[Bearbeiten] Unbenutzte Funktionen und/oder Variablen entfernenF: 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. [Bearbeiten] Optimierung der AusführungsgeschwindigkeitHierzu 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.
[Bearbeiten] Fußnoten
[Bearbeiten] LinksAtmel AVR4027: Tips and Tricks to Optimize Your C Code for 8-bit AVR Microcontrollers (Beispiel-Code) |