Codeoptimierung mit struct

 Startseite | Blog | Sitemap | Impressum | Login

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 Beispiel

Es soll ein 6 Byte langer ARP-Header erzeugt werden. Dieser sieht wie folgt aus:

2 Byte   Hardware type                   0x0001 (Ethernet)
2 Byte Protocol type 0x0800 (IP)
1 Byte Length of hardware addresses 0x06
1 Byte Length of protocol addresses 0x04

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: memcpy

Am einfachsten erscheint es, die Daten im RAM vorzubereiten und bei Bedarf an die richtige Adresse zu kopieren.

uint8_t arpHeader[] = {
0x00, 0x01,
0x08, 0x00,
0x06,
0x04
};

uint8_t *fillARPHeader(uint8_t *buff) {
memcpy(buff, arpHeader, 6);
return buff+6;
}

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
54: df 93 push r29
56: fc 01 movw r30, r24
58: 86 e0 ldi r24, 0x06 ; 6
5a: a0 e6 ldi r26, 0x60 ; 96
5c: b0 e0 ldi r27, 0x00 ; 0
5e: ef 01 movw r28, r30
60: 0d 90 ld r0, X+
62: 09 92 st Y+, r0
64: 8a 95 dec r24
66: e1 f7 brne .-8 ; 0x60
68: cf 01 movw r24, r30
6a: 06 96 adiw r24, 0x06 ; 6
6c: df 91 pop r29
6e: cf 91 pop r28
70: 08 95 ret

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 struct

Das 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 {
uint16_t hardwareType;
uint16_t protocolType;
uint8_t hardwareSize;
uint8_t protocolSize;
} ARP_Header;

uint8_t *fillARPHeader_3(uint8_t *buff) {
ARP_Header *ah;
ah=(ARP_Header*)buff;
ah->hardwareType = 0x0001;
ah->protocolType = 0x0800;
ah->hardwareSize = 6;
ah->protocolSize = 4;
return buff+6;
};

Der erzeugte Code zeigt, dass die Compileroptimierungen sehr gut funktionieren:

  8c:	fc 01       	movw	r30, r24
8e: 81 e0 ldi r24, 0x01 ; 1
90: 90 e0 ldi r25, 0x00 ; 0
92: 91 83 std Z+1, r25 ; 0x01
94: 80 83 st Z, r24
96: 80 e0 ldi r24, 0x00 ; 0
98: 98 e0 ldi r25, 0x08 ; 8
9a: 93 83 std Z+3, r25 ; 0x03
9c: 82 83 std Z+2, r24 ; 0x02
9e: 86 e0 ldi r24, 0x06 ; 6
a0: 84 83 std Z+4, r24 ; 0x04
a2: 84 e0 ldi r24, 0x04 ; 4
a4: 85 83 std Z+5, r24 ; 0x05
a6: cf 01 movw r24, r30
a8: 06 96 adiw r24, 0x06 ; 6
aa: 08 95 ret

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üllen

Die Optimierung über das Z-Register kann man noch etwas weiter treiben, indem sequentiell auf die Daten zugegriffen wird:

uint8_t *fillARPHeader(uint8_t *buff) {
// Hardware type: 0x00, 0x01 - Ethernet
*buff = 0x00;
buff++;
*buff = 0x01;
buff++;

// Protocol type: 0x80, 0x00 - IP
*buff = 0x80;
buff++;
*buff = 0x00;
buff++;

// Size of hardware addresses: 6 Byte
*buff = 6;
buff++;

// Size of protocol address: 4 Byte
*buff = 4;
buff++;

return buff;
}

Daraus erzeugt der Compiler einen erstaunlich kompakten Code, der aus lediglich 13 Befehlen besteht:

   72:	fc 01       	movw	r30, r24
74: 11 92 st Z+, r1
76: 81 e0 ldi r24, 0x01 ; 1
78: 81 93 st Z+, r24
7a: 80 e8 ldi r24, 0x80 ; 128
7c: 81 93 st Z+, r24
7e: 11 92 st Z+, r1
80: 86 e0 ldi r24, 0x06 ; 6
82: 81 93 st Z+, r24
84: 84 e0 ldi r24, 0x04 ; 4
86: 81 93 st Z+, r24
88: cf 01 movw r24, r30
8a: 08 95 ret

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.

Fazit

Die 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 Seite

Diese Seite wurde archiviert, d.h. sie wird nicht mehr aktiv gepflegt und die Informationen entsprechen unter Umständen nicht mehr dem aktuellen Stand.

Werbung
Look-Out
Talking about everything
Crazy audio
DIY audio projects and more
Anmesty International SchweizMenschenrechte für alle

Menschen für MenschenKarlheinz Böhms Äthiopienhilfe