
|
Sie befinden sich hier: Elektronik / Codeoptimierung mit struct /
Die AVR Hardwarearchitektur ist auf die Nutzung von C-Compilern optimiert - sagt zumindest der Hersteller. Wie sieht dies an einem konkreten Beispiel aus? Bei vielen komplexeren Datenstrukturen bietet sich zur Implementierung das C-Konstrukt struct an. Der Code wird dadurch recht gut les- und wartbar. Aber ist das auch die ideale Code in Bezug auf Code-Grösse und Geschwindigkeit? Gerade im Mikrocontroller-Umfeld muss der schönste Code nicht immer der beste sein (leider). Allgemein lässt sich das kaum beantworten, daher soll dies hier nur an einem konkreten Beispiel untersucht werden. Auf anderen Hardware/Compiler-Plattformen können die Ergebnisse völlig verschieden aussehen, weshalb dieser Artikel lediglich Anregungen geben soll, die nicht verallgemeinert werden dürfen. Das BeispielEs soll ein 6 Byte langer ARP-Header erzeugt werden. Dieser sieht wie folgt aus: 2 Byte Hardware type 0x0001 (Ethernet) Die Funktion ist wie folgt definiert: uint8_t *fillARPHeader(uint8_t *buff) Die Daten werden in den übergebenen Puffer kopiert, als Ergebnis wird ein Pointer geliefert, der auf das nächste Byte nach den einkopierten Daten zeigt. Die einfachste Variante: memcpyAm einfachsten erscheint es, die Daten im RAM vorzubereiten und bei Bedarf an die richtige Adresse zu kopieren. uint8_t arpHeader[] = {
Der erzeugte Code ist nicht so kurz wie erwartet, da AVRGCC memcpy als Inline-Funktion einbindet und diverse Register auf den Stack gesichert werden müssen. 52: cf 93 push r28 Diese Variante bringt es auf 16 Befehle. Die Bearbeitungsdauer liegt deutlich höher, da die innere Schleife 6-mal durchlaufen werden muss. Zusätzlich muss im Startup-Code das Datenfeld initialisiert werden, wodurch weiterer Code erzeugt wird und zusätzlich 6 Byte RAM alloziert werden. Arbeiten mit structDas Hardwaredesign des ATMega bietet mit den X-,Y- und Z-Pointern drei Register, welche die Arbeit beim Ausfüllen eines Datenblockes unterstützen. Dies ist darin begründet, es einen Befehl gibt, der den Inhalt der Speicherzelle, auf die das Z-Register zeigt, mit einem Wert füllen kann und gleichzeitig den Z-Pointer um 1 erhöht. Dies eignet sich ideal für Optimierungen des Compilers. Wie sieht es in der Praxis aus? typedef struct {
Der erzeugte Code zeigt, dass die Compileroptimierungen sehr gut funktionieren: 8c: fc 01 movw r30, r24 Der Code ist genauso lang wie die memcpy-Variante und ohne Schleife, wird also in 16 Takten abgearbeitet und ist daher sogar schneller. Sequentielles AusfüllenDie Optimierung über das Z-Register kann man noch etwas weiter treiben, indem sequentiell auf die Daten zugegriffen wird: uint8_t *fillARPHeader(uint8_t *buff) {
Daraus erzeugt der Compiler einen erstaunlich kompakten Code, der aus lediglich 13 Befehlen besteht: 72: fc 01 movw r30, r24 Am erzeugten Assemlber-Code sieht man sehr gut, wie das Z-Register für das Fortlaufende Ausfüllen des Puffers genutzt wird. Etwas Erstaunliches sieht man noch: Für das Auffüllen eines Bytes mit 0 wird das Register r1 benutzt (was wohl den Wert 0 enthält). Erstaunlich ist dies deshalb, weil bei der Arbeit mit struct diese Konstruktion bei 16-bit-Variablen nicht benutzt wurde. Hier bestehen also noch Optimierungsmöglichkeiten beim Compiler selbst. Wenn diese Möglichkeit schon im vorrangegangenen Beispiel vom Compiler benutzt worden wäre, wäre der Code nochmals 2 Befehle kürzer. Der hier gezeigte C-Code gehört sicher nicht zu den schönsten Programmen, die es gibt, dennoch ist der Code mit genügenden Kommentaren immer noch einigermassen lesbar. Ob sich die Ersparnis von 3 Befehlen/Takten lohnt, muss jeder für sich entscheiden. Angemerkt werden muss aber, dass diese Art der Optimierung nur funktioniert, wenn wirklich alle Daten eines Datenblockes lückenlos geschrieben werden müssen, was oft nicht der Fall ist. FazitDie Hardware des AVR ist durch das Z-Register wirklich sehr gut auf C-Strukturen angepasst, wodurch der Compiler kurzen und gleichzeitig schnellen Code erzeugen kann. Beim Einsatz von C erscheint die Nutzung von Strukturen als bester Kompromiss auch in Bezug auf den erzeugten Maschinencode. Die Funktion memcpy() sollte wirklich nur für grosse Datenblöcke genutzt werden, da ansonsten der Overhead im Maschinencode deutlich spürbar ist. Es gibt natürlich noch andere Sachen, auf die man achten sollte, z.B. die Arbeit mit breiten Datentypen. Archivierte SeiteDiese Seite wurde archiviert, d.h. sie wird nicht mehr aktiv gepflegt und die Informationen entsprechen unter Umständen nicht mehr dem aktuellen Stand. |