Ein wichtiges Merkmal eines Compilers ist die Güte des erzeugten Codes.
Immerhin will man seine Hardware optimal nutzen, und die geschriebenen Programme sollen möglichst wenig Laufzeit brauchen und möglichst wenig Speicher – also RAM und Flash – belegen.
Ein Vergleich der erzeugten Codes ist jedoch nicht einfach, denn ein Problem kann bereits innerhalb ein und der selben Programmiersprache auf sehr unterschiedliche Art und Weisen formuliert oder gelöst werden.
Dieser Artikel versucht ansatzweise einen Codevergleich weit verbreiteter AVR-Compiler anhand sehr einfacher Aufgaben, die "geradeaus" und ohne Umschweife programmiert wurden.
Ein Vergleich der Programmierung von Hardware-Komponenten und Peripherie wie Timer-, UART- oder I2C-Module scheint dabei weniger interessant, denn obwohl die Codes zum Steuern dieser Komponente in unterschiedlichen Sprachen recht verschieden aussehen, werden sie doch auf die selben Maschinen-Codes abgebildet, die sich im wesentlichen auf das Setzen und Lesen von Registern (SFRs) reduzieren. Neben diesen für jedes Programm essenziellen Abschnitten, besteht ein Programm aber zum großen Teil aus hardwareunabhängigen Aufgaben wie Registerverwaltung, Funktionsaufrufen, Schleifen, Abfragen, Zuweisungen, Parameterübergaben, Abfragen, Arithmetik, etc.
Interessanter erscheint ein Vergleich einfacher Aufgaben, die erkennen lassen, wie gut ein Compiler in der Lage ist, die Ressourcen eines Mikrocontrollers zu nutzen bzw. zu schonen.
Rahmenbedingungen
- avr-gcc
- Die Assembler-Ausgaben wurden mit der Optimierungsstufe "auf Größe optimieren" (-Os) für einen ATmega8 erstellt. GCC-Version war 3.4.x. Andere Optimierungsstufen haben wenig bis keinen Einfluss auf den erzeugtenCode. Codes für andere Controller der ATmega-Familie unterscheiden sich praktisch nicht vom ATmega8-Code.
- Bascom
Summe der ersten n Zahlen
Berechnet wird die Summe der ersten n Zahlen:
- [math]
\operatorname{sum}(n) \,=\, \sum_{k=1}^n k
\,=\, 1 + 2 + \ldots + n
[/math]
Die Zahl n wird als 16-Bit Zahl angegeben und das Ergebnis als 16-Bit-Zahl berechnet. Ein eventueller Überlauf wird nicht beachtet.
Der Code wird jeweils als eigene Funktion implementiert, um Abhängigkeiten vom umliegenden Code zu vermeiden.
Für diese Berechnung gibt es mehrere Möglichkeiten.
Aufsummieren in einer Schleife
Quellcodes:
avr-gcc
unsigned int
sum_n_loop (unsigned int n)
{
unsigned int sum = 0;
unsigned int i;
for (i=n; i > 0; i--)
sum += i;
return sum;
}
|
|
|
|
Compilat:
avr-gcc
sum_n_loop:
ldi r18,lo8(0)
ldi r19,hi8(0)
.L12:
sbiw r24,0
breq .L11
add r18,r24
adc r19,r25
sbiw r24,1
rjmp .L12
.L11:
movw r24,r18
ret
|
|
|
|
Berechnung mit rekursiver Funktion
Quellcodes:
avr-gcc
unsigned int
sum_n_rekursiv (unsigned int n)
{
if (n == 0)
return 0;
return n + sum_n_rekursiv (n-1);
}
|
|
|
|
Compilat:
avr-gcc
sum_n_rekursiv:
push r28
push r29
movw r28,r24
sbiw r24,0
breq .L13
sbiw r24,1
rcall sum_n_rekursiv
add r24,r28
adc r25,r29
.L13:
pop r29
pop r28
ret
|
|
|
|
Berechnung durch Formel
Quellcodes:
avr-gcc
unsigned int
sum_n_formel (unsigned int n)
{
return n*(n+1) / 2;
}
|
|
|
|
Compilat:
avr-gcc
sum_n_formel:
mul r24,r24
movw r18,r0
mul r24,r25
add r19,r0
mul r25,r24
add r19,r0
clr r1
add r18,r24
adc r19,r25
movw r24,r18
lsr r25
ror r24
ret
|
|
|
|
Interrupt-Routinen
Auch diese Beispiele machen nicht viel. Das erste zählt nur eine 16-Bit Variable hoch, das zweite macht nichts weiter, als ein Funktionsaufruf.
Eine Variable hochzählen
Quellcodes:
avr-gcc
#include <avr/io.h>
#include <avr/signal.h>
int volatile count;
SIGNAL (SIG_OVERFLOW0)
{
count++;
}
|
|
|
|
Compilat:
avr-gcc
__vector_9:
push __zero_reg__
push __tmp_reg__
in __tmp_reg__,__SREG__
push __tmp_reg__
clr __zero_reg__
push r24
push r25
lds r24,count
lds r25,(count)+1
adiw r24,1
sts (count)+1,r25
sts count,r24
pop r25
pop r24
pop __tmp_reg__
out __SREG__,__tmp_reg__
pop __tmp_reg__
pop __zero_reg__
reti
|
|
|
|
Eine Funktion aufrufen
Quellcodes:
avr-gcc
#include <avr/io.h>
#include <avr/signal.h>
extern void foo();
SIGNAL (SIG_OVERFLOW1)
{
foo();
}
|
|
|
|
Compilat:
avr-gcc
__vector_8:
push __zero_reg__
push __tmp_reg__
in __tmp_reg__,__SREG__
push __tmp_reg__
clr __zero_reg__
push r18
push r19
push r20
push r21
push r22
push r23
push r24
push r25
push r26
push r27
push r30
push r31
rcall foo
pop r31
pop r30
pop r27
pop r26
pop r25
pop r24
pop r23
pop r22
pop r21
pop r20
pop r19
pop r18
pop __tmp_reg__
out __SREG__,__tmp_reg__
pop __tmp_reg__
pop __zero_reg__
reti
|
|
|
|
Sortieren mit Bubble-Sort
Zum Abschluss noch ein komplexeres Beispiel: Ein Array mit 8-Bit-Werten soll mit Bubble-Sort der Größe nach sortiert werden.
Im Array wird nach dem größten Wert gesucht und dieser ans Ende getauscht. Danach macht man den Teilbereich, in dem man das Maximum sucht, um 1 kleiner, bis man fertig ist. Die größten Zahlen wandern wie Blasen nach oben, daher der Name Bubble-Sort für diesen Sortier-Algorithmus.
Variablen:
- n Array-Größe
- a das Array
- i Größe der Teilbereichs
- j Laufvariable durch den Teilbereich
Quellcodes:
avr-gcc
#include <limits.h>
#include <inttypes.h>
void
bubble_sort (uint8_t n, char * a)
{
uint8_t i;
// Bereich schrumpft von n auf 2
for (i = n; i >= 2; i--)
{
char max = CHAR_MIN;
uint8_t j, j_max;
// im Bereich nach MAX suchen
for (j = 0; j < i; j++)
{
if (a[j] >= max)
{
// MAX und Pos. merken
j_max = j;
max = a[j_max];
}
}
// MAX ans Ende tauschen
a[j_max] = a[i-1];
a[i-1] = max;
}
}
|
|
|
|
Compilat:
avr-gcc
bubble_sort:
cpi r24,2 ; i >= 2
brlo .L12
movw r26,r22 ; $6 = a
add r26,r24 ; $6 += i
adc r27,__zero_reg__
sbiw r26,1 ; $6--
.L10: ; { i
ldi r20,-128 ; max = -128
ldi r19,0 ; j = 0
cp r19,r24 ; j < i
brsh .L14
movw r30,r22 ; $3 = a
.L9: ; { j
ld r18,Z+ ; $4 = * $3++
cp r18,r20 ; $4 >= max
brlt .L7 {
mov r21,r19 ; j_max = j
mov r20,r18 ; max = $4
.L7: }
subi r19,-1 ; j++
cp r19,r24 ; j < i
brlo .L9
.L14: ; } j
movw r30,r22 ; $5 = a
add r30,r21 ; $5 += j_max
adc r31,__zero_reg__
ld r18,X ; $7 = * $6
st Z,r18 ; * $5 = $7
st X,r20 ; * $6 = max
subi r24,1 ; i--
sbiw r26,1 ; $6--
cpi r24,2 ; i >= 2
brsh .L10
.L12: ; } i
ret
|
|
|
|
Die Codes wurden etwas nachkommentiert und -formatiert, um besser den Zusammenhang mit der Quelle durchblicken zu können.
Siehe auch