Forum: Compiler & IDEs Spaßig: "% 16" vs. "& 0x0F"


von johnny.m (Gast)


Lesenswert?

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

von A.K. (Gast)


Lesenswert?

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.

von Owz (Gast)


Lesenswert?

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

von johnny.m (Gast)


Lesenswert?

...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?

von Rahul, der Trollige (Gast)


Lesenswert?

>...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...

von johnny.m (Gast)


Lesenswert?

@Rahul:
> Müsste es nicht eigentlich "x % 2^n" entspricht "x & 2^n-1" heissen?
Habs auch grad gemerkt. Bin noch nicht ganz wach.
1
for(int i = 0; i < 100; i++)
2
    printf("0Fh ist 15d und nicht 16d!!!!");

von Karl H. (kbuchegg)


Lesenswert?

> 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.

von Fritz (Gast)


Lesenswert?

Ein anderer, käuflicher Compiler macht in beiden Fällen ein
andi r16,15.
Letztlich sind die paar Bytes mehr Code eher ein Schönheitsfehler.

von A.K. (Gast)


Lesenswert?

> Alles wird im größten Datentyp gerechnet, der im
> Ausdruck vorkommt.

... mindestens aber als "int" oder "unsigned".

von johnny.m (Gast)


Lesenswert?

>> 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).

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von A.K. (Gast)


Lesenswert?

> 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.

von Matthias (Gast)


Lesenswert?

...GCC kennt -mint8, damit ist sizeof(int)==sizeof(char). Die avrlib 
kannst
du dann allerdings vergessen....

Was ist damit gemeint??

von Karl H. (kbuchegg)


Lesenswert?

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.

von A.K. (Gast)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.