Mal was Grundsätzliches zum AVR-GCC:
Ich benutze in einem Programm einen Zykluszähler (uint8_t count_cycles),
der in einem Timer-Interrupt dekrementiert wird. Dieser Zykluszähler
wird für zwei Auswertungen benötigt, von denen eine alle 256 Zyklen
(Ausgabe auf Display) und die andere alle 16 Zyklen (Tasterabfrage)
durchgeführt werden soll. Mit den 256 Zyklen ist keine Frage, da wird
einfach der Zähler auf "0" überprüft. Die 16 Zyklen kann man prinzipiell
mit
1
if(!(count_cycles%16))
2
{
3
//Tasterabfrage
4
}
hinbekommen. Ich hab mir dann gedacht, dass "x % 2^n" ja nichts anderes
ist als "x & 0x0F". Und da das Programm für einen Laborversuch für
Studis ist, die auch ein bisschen ein Gefühl dafür bekommen sollen, wie
man welche arithmetischen oder logischen Operationen gegeneinander
austauschen kann, um evtl. auch den Code effizienter zu gestalten, habe
ich dann natürlich geschrieben
1
if(!(count_cycles&0x0F))
2
{
3
//Tasterabfrage
4
}
...und mir gedacht, dass das sicher eher weniger Assembler-Code gibt als
die Schreibweise mit Modulo. Hab dann auch nicht weiter drüber
nachgedacht. Irgendwann kam mir die spaßige Idee, einfach mal
auszuprobieren, was der Compiler denn aus der Modulo-Version macht. Und
da kam die große Überraschung: Die "& 0x0F"-Variante ergeben satte 8
Bytes mehr Code als die mit "% 16"! Und ich war davon ausgegangen,
dass eher weniger dabei rauskommt, aber auf keinen Fall mehr. Habe dann
mal in die entsprechenden List-Files geschaut und folgendes gefunden:
1
if(!(count_cycles & 0x0F))
2
be: 89 2f mov r24, r25
3
c0: 99 27 eor r25, r25
4
c2: 8f 70 andi r24, 0x0F ; 15
5
c4: 90 70 andi r25, 0x00 ; 0
6
c6: 89 2b or r24, r25
7
c8: 51 f4 brne .+20 ; 0xde <__vector_3+0x48>
...für die &-Variante und
1
if(!(count_cycles % 16))
2
be: 9f 70 andi r25, 0x0F ; 15
3
c0: 51 f4 brne .+20 ; 0xd6 <__vector_3+0x40>
...für Modulo. In beiden Fällen ist Optimierung -Os eingestellt.
Was mich jetzt wirklich verwundert, ist, dass der Compiler anscheinend
aus der Modulo-Version exakt das macht, was ich mir vorgestellt habe,
nämlich ein einfaches "& 0x0F". Was er allerdings aus der "&
0x0F"-Version macht, leuchtet mir nicht ganz ein. Liegt das evtl. daran,
dass die einfachen logischen und arithmetischen Operationen
grundsätzlich mindestens in 16 Bit gerechnet werden? Und Division,
Modulo usw. nur in 8 Bit? Wenn dem so ist, würde ich natürlich gerne
generell wissen, welche Operationen in 16 und welche in 8 Bit
verwurstet werden. Gerade wenn man solche Abfragen in zeitkritischem
Umfeld macht, sollte man ja die kürzeste Variante nehmen.
Gruß
Johnny
Die Spezifikation ist immer die gleiche, d.h. die Rechnung muss so
erfolgen, als ob sie 16bittig sei (int/unsigned). Es liegt am Compiler,
ob er daraus den effizientesten Coder generiert oder nicht.
Die allgmeinen Optimierungsreglen in GCC sind naturgemäss nicht darauf
aus, Wortoperationen durch Byteoperationen zu ersetzen. Das ist für fast
alle Targets sinnlos. Das wird folglich irgendwie im AVR-spezifischen
Teil abgewickelt. Und greift offensichtlich nicht immer.
Hallo!
Was der Compiler wirklich draus baut ist schwer vorher zu sagen. Ich
machs immer so, wenn ich über so was stolper, ich schau mir den Code
ohne Optimierung an, dann kommt man oft drauf, was sich der Compiler
dabei denkt!
mfg Owz
...Sollte oben natürlich heißen "x % 2^n" entspricht "x & 2^n" (und
nicht "x & 0x0F")...
@Owz:
> ...ich schau mir den Code ohne Optimierung an...
Wieso ohne Optimierung? Sehe ich so keinen Grund für. Was der Compiler
sich denkt, kann ich bei den o.g. Beispielen auch sehen. Ich sehe z.B.
dass der Compiler bei "%" an 8 Bit denkt und bei "&" an 16 Bit... Oder
meinst Du was anderes?
>...Sollte oben natürlich heißen "x % 2^n" entspricht "x & 2^n" (und>nicht "x & 0x0F")...
Müsste es nicht eigentlich "x % 2^n" entspricht "x & 2^n-1" heissen?
Das Problem liegt doch darin, wie sich der Mensch, der den Compiler
gebaut hat, die Umsetzung überlegt hat (Also eine Frage an Bruce und
Jörg...). Vielleicht gibt es aber auch im C-Standard eine Regel, wie man
das realisiert - davon habe ich natürlich keine Ahnung, da ich keine
Compiler baue...
Interessant ist es aber trotzdem, vor allem, weil ich meine Puffer immer
mit "& 2^n-1" begrenze...
> Vielleicht gibt es aber auch im C-Standard eine Regel, wie man> das realisiert
Die Regel ist ganz einfach:
Alles wird im größten Datentyp gerechnet, der im Ausdruck
vorkommt. Eine Zahl ohen weiteren Suffix ist ein int, es sei
denn die Zahl ist so gross dass sie nicht mehr in einen
int hineinpasst.
D.h. sowohl
i & 0x0F
als auch
i % 16
ist mal grundsätzlich als int abzuhandeln. Nun gibt es natürlich
noch den Passus Optimierung: Ein Compiler darf optimieren, solange
im Ergebnis nicht sichtbar ist, dass optimiert wurde.
Irgendwelche Leute haben sich jetzt überlegt, dass ein %16 auch
durch eine & Operation dargestellt werden kann. Sie haben sich
auch weiter überlegt, dass das Ergebnis nicht größer als 16 sein
kann und daher eine int-Operation überflüssig ist. Es reicht
wenn man das ganze auf dem low-byte macht. Es gibt viele
derartige Abkürzungen, schau dir nur mal an wie ein Compiler
Multiplikationen mit kleinen Zahlen realisiert.
Beim Fall &0x0F hat aber anscheinend noch niemand eine entsprechende
Optimierung eingebaut, die ähnliches leistet: den Compiler von
int auf char zurückschalten, weil das Ergebnis nicht größer
als char sein kann.
>> Alles wird im größten Datentyp gerechnet, der im>> Ausdruck vorkommt.> ... mindestens aber als "int" oder "unsigned".
Das war auch mein Kenntnisstand. Deshalb hatte ich mich ja gewundert,
dass die Modulo-Operation anscheinend in 8 Bit gerechnet wird.
@Fritz:
Dass kommerzielle Compiler, die noch stärker auf die 8-Bit-CPUs
zugeschnitten sind, da u.U. noch mehr optimieren, war mir auch schon
aufgefallen (habe längere Zeit mit CodeVision programmiert).
johnny.m wrote:
>> ... mindestens aber als "int" oder "unsigned".> Das war auch mein Kenntnisstand. Deshalb hatte ich mich ja gewundert,> dass die Modulo-Operation anscheinend in 8 Bit gerechnet wird.
Der C-Standard ist ein "as if"-Standard: eine Implementierung muss sich
so verhalten, "als ob" sie die entsprechenden Vorschriften 1:1
implementiert hätte. Wenn das obere Byte zum Ergebnis nicht beitragen
kann, darf es also aus der Berechnung auch weggelassen werden.
> Dass kommerzielle Compiler, die noch stärker auf die 8-Bit-CPUs> zugeschnitten sind, da u.U. noch mehr optimieren,
Bisweilen aber elegant am Standard vorbei. Die "mindestens als int"
Regel wird bei propritären Compilern für 8-bit Microcontroller gerne
verletzt, um besseren Code zu erzeugen. Üblicherweise findet sich dann
eine Option, um zum Standard konform zu sein.
GCC kennt -mint8, damit ist sizeof(int)==sizeof(char). Die avrlib kannst
du dann allerdings vergessen.
Dass es möglich ist, int so umzudefinieren, dass es nicht
mehr 2 Bytes belegt sondern nur noch 1.
Was allerdings passiert, wenn du einen solchen int an eine
Funktion in der Standardlibrary übergibst, die 2 Bytes für
einen int erwartet, kannst du dir selbst ausmalen.
Mit -mint8 lässt sich der Compiler so einstellen, das "int" und
"unsigned" 8-Bit-Typen sind. Die anderen Typen verschieben sich dann
auch (=> GCC Manual, evtl. in 4.x anders als 3.x).
Der Code ist entsprechend effizienter, weil ebendiese erzwungenen und
nicht immer wegoptimierten 8=>16-Bit Konvertierungen entfallen.
Natürlich ist das nicht zum C-Standard konform, und alle hinzugelinkten
Funktionen müssen ebenso übersetzt sein, jedenfalls solange sie in den
Parametern und Return-Werten nicht durchgängig mit Typen wie "int16_t"
arbeiten.