Schlanke Embeded-Entwicklung mit Small C++

Seite 3: Bit für Bit

Inhaltsverzeichnis

Ein Versuch, weitere Byte dadurch einzusparen, dass delay_ms nicht inline definiert wird, sondern extern aus einer Library gelinkt wird, ist dagegen kontraproduktiv. Warum eigentlich? Die Antwort darauf liefert ein Vergleich der Assembler-Listings der beiden Varianten (siehe Exkurs "Dem Compiler auf die Finger schauen"). Dabei interessiert zuerst die Funktion ledHalfCycle.

Das folgende Listing zeigt den ersten Teil der Funktion mit dem (inline) Ein- und Ausschalten der LED.

00000086 <_Z12ledHalfCycleILi200EEvb>:
template <int ms>
void ledHalfCycle(bool on)
{
if (on) {
86: 88 23 and r24, r24
88: 11 f0 breq .+4 ; 0x8e

inline void ledOn()
{
PORTB |= _BV(PB5); // der then-Zweig schaltet ein
8a: 2d 9a sbi 0x05, 5 ; 5
8c: 01 c0 rjmp .+2 ; 0x90

inline void ledOff()
{
PORTB &= ~_BV(PB5); // der else-Zweig schaltet aus
8e: 2d 98 cbi 0x05, 5 ; 5

Bis hier sind beide Varianten identisch. Danach kommt in der zweiten Variante mit der Library-Funktion der Aufruf von delay_ms() mit der Übergabe _delay_ms_ticks mit _loops (0x07D0) und _ticks (0x0190):

    delay_ms(ms);
a8: 60 ed ldi r22, 0xD0 ; 208
aa: 77 e0 ldi r23, 0x07 ; 7
ac: 80 e9 ldi r24, 0x90 ; 144
ae: 91 e0 ldi r25, 0x01 ; 1
b0: 0e 94 43 00 call 0x86 ; 0x86 <_Z8delay_ms15_delay_ms_ticks>

In der ersten Variante wird delay_ms inline expandiert:

  90   80 ed           ldi     r24, 0xD0       ; 208
92: 97 e0 ldi r25, 0x07 ; 7
__asm__ volatile (
"1: sbiw %0,1" "\n\t"
"brne 1b"
: "=w" (__count)
: "0" (__count)
);
94: 20 e9 ldi r18, 0x90 ; 144
96: 31 e0 ldi r19, 0x01 ; 1
98: f9 01 movw r30, r18
9a: 31 97 sbiw r30, 0x01 ; 1
9c: f1 f7 brne .-4 ; 0x9a
<_Z12ledHalfCycleILi200EEvb+0x14>
9e: 01 97 sbiw r24, 0x01 ; 1
};

inline void
delay_ms(_delay_ms_ticks _d)
{
while (_d._loops)
a0: 00 97 sbiw r24, 0x00 ; 0
a2: d1 f7 brne .-12 ; 0x98
<_Z12ledHalfCycleILi200EEvb+0x12>
ledOn();
} else {
ledOff();
}
delay_ms(ms);
}

Dabei wird erst mit zwei 8-Bit-Loads _loops für die while-Schleife geladen. Dann kommt die Funktion _delay_loop_2, die im Headerfile als inline-Assembler definiert ist und eine einfache Zählschleife implementiert (bis brne .-4) und darauf das Herunterzählen der äußeren Schleife mit zwei Subtraktionen und dem bedingten Sprung.

Die zweite Subtraktion (sbiw r24, 0x00) ist vermutlich ein Artefakt des Compilers und müsste eigentlich wegoptimiert werden. Danach folgt noch das Return, das in beiden Varianten identisch ist. Wenn man das Laden von _delay_ms_ticks, das in beiden Varianten anfällt, nicht berücksichtigt, kommt man bei der Variante mit der externen Funktion auf 4 Byte für den Funktionsaufruf und bei der Inline-Variante auf 12 Byte für die expandierte Funktion (und damit auf genau die 8 Byte Differenz, die das Mapfile schon gezeigt hat).

Interessant ist hier vor allem, dass die Inline-Expansion von delay_ms nur 12 Byte benötigt, die externe Funktion (wie im Mapfile zu sehen) jedoch 24 Byte. Zur Erklärung des Unterschieds wird delay_ms in der externen Variante genauer analysiert:

00000086 <_Z8delay_ms15_delay_ms_ticks>:
void delay_ms(_delay_ms_ticks d)
{
86: 9b 01 movw r18, r22
88: ac 01 movw r20, r24
while (d._loops)
8a: 21 15 cp r18, r1
8c: 31 05 cpc r19, r1
8e: 31 f0 breq .+12 ; 0x9c
<_Z8delay_ms15_delay_ms_ticks+0x16>
_delay_loop_2(d._ticks);

__asm__ volatile (
"1: sbiw %0,1" "\n\t"
"brne 1b"
: "=w" (__count)
: "0" (__count)
);
90: ca 01 movw r24, r20
92: 01 97 sbiw r24, 0x01 ; 1
94: f1 f7 brne .-4 ; 0x92
<_Z8delay_ms15_delay_ms_ticks+0xc>
--d._loops;
96: 21 50 subi r18, 0x01 ; 1
98: 31 09 sbc r19, r1
9a: f7 cf rjmp .-18 ; 0x8a
<_Z8delay_ms15_delay_ms_ticks+0x4>
}
9c: 08 95 ret

In der Funktion werden zuerst mit zwei 16-Bit-Moves die Parameter gerettet und anschließend mit zwei 8-Bit-Vergleichen die While-Bedingung am Anfang getestet. Eine 16-Bit-Addition adiw r18, 0 wäre für die Codegröße allerdings günstiger – für die Laufzeit ergeben die beiden Varianten dagegen keinen Unterschied. Die Expansion von _delay_loop_2 (Adressen 0x90 bis 0x95) ist im Prinzip identisch. Danach wird die äußere Schleife heruntergezählt (Adressen 0x96 bis 0x99).

Warum hier zwei 8-Bit-Subtraktionen statt einer 16-Bit-Subtraktion verwendet werden, ist unklar. Aber da der Compiler bei der Inline-Variante ebenfalls zwei Byte zu viel Code generiert, gibt das hier keinen Unterschied. Und weil der While-Test am Anfang stattfindet, wird zum Schluss ein unbedingter Sprung benötigt. Von den 12 Byte Unterschied zwischen der Inline- und der externen Variante gehen also 6 Byte zulasten des Funktionsaufrufs (2 Byte für das Return und 4 Byte für die Parameterkopie, wovon 2 Byte eigentlich unnötig sind) und 6 Byte zulasten des Vergleichs am Anfang der While-Schleife (wovon wieder zwei Byte ungünstiger Codegenerierung zuzuschreiben sind). Die Inline-Variante kann sich den Test am Anfang der While-Schleife sparen, weil zur Compile-Zeit bekannt ist, dass der Wert von _loops nicht 0 ist. Selbst bei günstiger Codegenerierung ist die Inline-Variante also kleiner, da sie Informationen aus der Aufrufumgebung mit berücksichtigen kann.

Noch unklar ist der Unterschied bei main(). Eigentlich sollte main() bei der Inline-Variante genauso 8 Byte größer sein wie ledHalfCycle<200>(bool on), stattdessen ist das main() der externen Variante um 4 Byte größer. Bei der Analyse der beiden main() in den Assembler-Listings stellt man fest, dass der Compiler in der Inline-Variante schlicht besseren Code erzeugt, während in der zweiten Variante die Parameter für den externen Funktionsaufruf unnötig durch verschiedene Register kopiert werden.

Es kostet schon etwas Aufwand, dem Compiler genau auf die Finger zu sehen und zu kontrollieren, was für ein Code erzeugt wird. Als Lohn versteht man besser, wie die verschiedenen C- und C++-Konstrukte in Assembler umgesetzt werden.

Im Beispiel des Artikels zeigt sich, dass einerseits der (GCC-)Compiler bei externen Funktionsaufrufen Mühe hat, größenoptimierten Code zu erzeugen, und andererseits die Kenntnis der Parameterwerte bei inline-Aufrufen bessere Optimierung ermöglicht. Mit dieser (inline-)Version ist das Programm zwar schön klein, aber nach C++ sieht es noch nicht wirklich aus (trotz template und constexpr). Der wesentliche Vorteil von C++ gegenüber C ist die Möglichkeit, einfacher Abstraktionen zu entwickeln, die sich dann immer wieder verwenden lassen. In diesem Einfachst-Programm bietet sich die LED-Ansteuerung an. Da sie sich (programmtechnisch) nicht von anderen allgemeinen digitalen Ausgängen unterscheidet, kann man hierfür eine Klasse GpioOut definieren.