Hallo,
ich habe mir gerade etwas Code angeguckt, der vom GCC prouziert worden
ist. Dabei wunder ich mich darüber, dass adiw genutzt wird um einen
uint8_t zu inkrementieren.
Bei writer_out handelt es sich um einen static uint8_t.
Beispiel 1 (#define TX_BUFFER_SIZE (32)):
1
wirter_out = (wirter_out + 1) % TX_BUFFER_SIZE;
2
39c: 01 96 adiw r24, 0x01 ; 1
3
39e: 8f 71 andi r24, 0x1F ; 31
4
3a0: 90 70 andi r25, 0x00 ; 0
5
3a2: 80 93 84 00 sts 0x0084, r24
Beispiel 2 (#define TX_BUFFER_SIZE (255)):
1
wirter_out = (wirter_out + 1) % TX_BUFFER_SIZE;
2
39c: 01 96 adiw r24, 0x01 ; 1
3
39e: 6f ef ldi r22, 0xFF ; 255
4
3a0: 70 e0 ldi r23, 0x00 ; 0
5
3a2: 0e 94 57 04 call 0x8ae ; 0x8ae <__divmodhi4>
6
3a6: 80 93 84 00 sts 0x0084, r24
Warum wird für writer_out 2 Register verwendet? Was für ein Call
geschieht im zweiten Beispiel, bzw. wofür ist der gut? Ist der nicht
überflüssig?
Der Call hat eine ganz schön tiefe Verschalchtelung. Frisst das nicht
massiv Rechenzeit?
Habe Optimierung -Os und WINAVR 20090313 und das Programm wird für einen
ATmega32 übersetzt.
Der Code von __divmodhi4 sieht so aus:
meines wissens ist gcc nicht für 8bit µc gemacht, sondern wird vielmehr
zweckentfremdet. es sit eigentlich für 16bit prozessoren gemacht, d.h.
jede 8bit variable wird erst einmal in eine 16 bit variable aufgebohrt
und belegt dadurch auch 2 register
ps: für diese aussage lege ich jetzt aber nicht meine hand ins feuer
tobi schrieb:
> Hallo,>> ich habe mir gerade etwas Code angeguckt, der vom GCC prouziert worden> ist. Dabei wunder ich mich darüber, dass adiw genutzt wird um einen> uint8_t zu inkrementieren.
gcc ist bekannt dafür manchmal 16 Bit Arithmetik zu benutzen, wenn
eigentlich 8 Bit auch reichen würden.
Dies kommt daher, weil C vorschreibt, dass Ganzzahlarithmetik
zuallererst einmal int-Arithmetik bedeutet. Wenn 8 Bit reichen würden
ist es Sache des Compilers das zu merken und zu optimieren.
> Beispiel 2 (#define TX_BUFFER_SIZE (255)):>
>> Warum wird für writer_out 2 Register verwendet? Was für ein Call> geschieht im zweiten Beispiel, bzw. wofür ist der gut? Ist der nicht> überflüssig?
Das wird die Modulo-Division sein. 255 ist keine 2-er Potenz
tobi schrieb:
> Warum wird für writer_out 2 Register verwendet?
Diese Rechnung muss 16bittig erfolgen, weil sonst bei writer_out=255 ein
falsches Ergebnis rauskommt. Denn writer_out+1 ist per C-Definition 256,
nicht 0.
Schonmal danke für die Antworten.
Das mit der 2er Potenz hatte ich nicht bedacht. Dann ist klar, dass dort
ein Subroutine genutzt werden muss.
Gibt es denn eine Optimierung, mit der der GCC wirklich nur 8 Bit
Arithmetik betreibt?
Habe mich bislang noch nicht wirklich mit den Optimierungen des avrgcc
befasst.
Das ist etwas zu komplex um hier alles aufzuzählen. Doch existiert in
der Runtime vermutlich keine 8-bit Divisionsroutine, insofern wird eine
echte Division immer auf 16-bit Rechnung hinauslaufen.
Anonsten hilft ein bischeh mitdenken. Mit
uint8_t x;
können x+1 und x<<1 über die Grenzen eines 8-bit Wertes hinauswachsen,
müssen also 16bittig gerechnet werden, x|1 und x>>1 jedoch nicht. Manche
dieser Erkenntnisse besitzt auch der Compiler und handelt entsprechend.
Wenn bekannt ist, dass x+1 nicht überlaufen wird, kann man u.U. mit
(uint8_t)(x+1) aushelfen.
tobi schrieb:
> Wird also immer ein Register mehr genommen als benötigt, um den Überlauf> abzufangen?
Definition von C: Arithmetik mit Typen kleiner als "int" muss in "int"
durchgeführt werden, oder zumindest muss das gleiche dabei rauskommen.
Und deshalb sind wie eben skizziert manche Operationen dabei etwas
teurer als andere.
>Diese Rechnung muss 16bittig erfolgen, weil sonst bei writer_out=255 ein>falsches Ergebnis rauskommt. Denn writer_out+1 ist per C-Definition 256,>nicht 0.
Und warum mach er dann das hier?
1
volatilestaticuint8_twirter_out=0;// damit der Optimierer noch was übrig lässt...
2
wirter_out=(++wirter_out)%32;
ergibt
1
92: 80 91 60 00 lds r24, 0x0060
2
96: 8f 5f subi r24, 0xFF ; 255
3
98: 80 93 60 00 sts 0x0060, r24 ; <= ist nur drin wegen volatile
4
9c: 80 91 60 00 lds r24, 0x0060 ; <= ist nur drin wegen volatile
Hmm... OK... das ist etwas unvorteilhaft für 8-Bitter.
Aber so sollte es dann besser sein:
1
writer_out = (++writer_out) % TX_BUFFER_SIZE;
2
39c: 8f 5f subi r24, 0xFF ; 255
3
39e: 8f 71 andi r24, 0x1F ; 31
4
3a0: 80 93 84 00 sts 0x0084, r24
Allerdings bekomm ich nun eine Warnung:
1
../bufferedUSART.c: In function 'usart_putc':
2
../bufferedUSART.c:181: warning: operation on 'writer_out' may be undefined
Das wird wohl genau den Grund haben, dass dort ein eventueller Überlauf
stattfinden kann, da nurnoch 8 Bit Arithmetik genutzt wird.
Gibt es hier sowas wie man es vom Javaprogrammieren in Eclipse kennt?
Also sowas wie @SuppressWarnings(...), womit man solche Warnungen
ausblenden kann?
An dieser Stelle weiß ich ja, dass die Warnung keinen bösen Effekt haben
wird, da ein "Überlauf", an dieser Stelle, nichts schlimmes wär.
tobi schrieb:
> Das wird wohl genau den Grund haben, dass dort ein eventueller Überlauf> stattfinden kann, da nurnoch 8 Bit Arithmetik genutzt wird.
Nein. Hier ist das Problem, dass zwei Zuweisungen erfolgen, bei denen
die Reihenfolge nicht definiert ist. Nämlich ++writer_out und
writer_out=... Das lässt sich als
writer_out = writer_out + 1;
writer_out = writer_out % ...
oder als
uint8_t temp = writer_out + 1;
writer_out = temp % 32;
writer_out = temp;
interpretieren.
Die Compiler-Meldung ist ein freundlicher hinweis auf genau diese
Zweideutigkeit.
Diese Methode braucht ein klein wenig länger als die Methode mit
Preinkrement und Modulo, aber dafür keine Warnung.
Wenn es keine Möglichkeit gibt, gezielt Warnungen zu unterdrücken, dann
werde ich wohl die letztere Nehmen.
Meines Wissens gibt es hier keinen Sequence-Point, der die komplette
Sequence definiert machen würde.
>> Diese Methode braucht ein klein wenig länger als die Methode mit> Preinkrement und Modulo, aber dafür keine Warnung.
Nur weil der Compiler keine Warnung ausspuckt, heißt das nicht, dass
diese Sequenz richtig ist. Sie hat immer noch undefiniertes Verhalten.
Als Merkregel: Wenn du in einem Ausdruck versuchst eine Variable 2mal zu
verändern, hast du undefiniertes Verhalten.
Genau das versuchst du
* einmal mittels ++writer_out
* das zweite mal durch die Zuweisung
> Wenn es keine Möglichkeit gibt, gezielt Warnungen zu unterdrücken, dann> werde ich wohl die letztere Nehmen.
Tus nicht
A. K. schrieb:
> Apropos: Peters Methode funktioniert nur bis max=255.
Nö, funktioniert auch bei 256.
Ich nehme sie hauptsächlich deshalb, weil ich dann nicht an 2-er
Potenzen gebunden bin, sondern beliebige Puffergrößen nehmen kann und
damit den Speicher besser auslasten kann.
Peter
Peter Dannegger schrieb:
> Nö, funktioniert auch bei 256.
Stimmt. Ist dann sogar recht effizient, weil das ?: komplett rausfliegt
und Überlauf/Wrap ersetzt wird. Nicht mein Stil, aber wer's mag... (bei
int8_t und 128 geht es schief).
Das Problem mit dem fehlenden Sequence Point ist dort immer noch drin.
Stammt das wirklich aus echtem Code?
tobi schrieb:
> Allerdings bekomm ich nun eine Warnung:>
1
> ../bufferedUSART.c: In function 'usart_putc':
2
> ../bufferedUSART.c:181: warning: operation on 'writer_out' may be
3
> undefined
4
>
Warum diese Warnung bei Deinem Code kommt, bei meinem aber nicht, kann
ich mir auch nicht erklären.
Sie dürfte erst dann kommen, wenn z.B. ++writer_out zweimal vorkommt und
damit unterschiedliche Ergebnisse rauskommen, je nachdem, welcher zuerst
ausgeführt wird.
Das ist aber bei unseren beiden Codes nicht der Fall, daß Ergebnis ist
immer eindeutig.
Der GCC warnt leider gerne mal auch an Stellen, wo es garnichts zu
warnen gibt. Ich ärgere mich z.B. regelmäßig über die Forderung nach
überflüssiger Klammerung.
Ich glaube nicht, daß es viel Sinn macht, bei überflüssigen Warnungen
einen Bugreport zu schreiben.
Peter
Peter Dannegger schrieb:
> Das ist aber bei unseren beiden Codes nicht der Fall, daß Ergebnis ist> immer eindeutig.
Hier liegst du falsch.
Das Ergebnis von
i = ++i;
ist genauso undefiniert, wie das von
i = ++i > KONSTANT ? 0 : i
oder Variationen davon. Auch wenn die Operation Preinkrement heißt, wird
der Compiler nicht dazu gezwungen i selbst zu verändern nachdem die
Erhöhung abgelaufen ist. Wenn der Compiler will, kann er das Ergebnis
von ++i erst ganz zum Schluss an i selbst zuweisen, nachdem alles andere
schon durchgeführt wurde.
Die entscheidende Frage ist, ob ?: einen sequence point darstellt. Das
Web gibt dazu unterschiedliche Informationen.
Wenn doch, dann ist x = ++x ? a : b; definiert, x = ++x; aber nicht.
Wenn nicht, dann sind beide gleich undefiniert, denn obzwar ein Compiler
das ++x gern vorher abschliessen wird ist dies dann nicht gewährleistet.
A. K. schrieb:
> Die entscheidende Frage ist, ob ?: einen sequence point enthält. Das Web> gibt dazu unterschiedliche Informationen.
Mein Gedächtnis kramt da nichts raus.
Laut Wiki wäre da einer.
http://en.wikipedia.org/wiki/Sequence_point
In dem Fall nehme ich alles zurück. Der ganze Ausdruck ist dann
wohldefiniert.
Karl heinz Buchegger schrieb:
> Das Ergebnis von>> i = ++i;>> ist genauso undefiniert
Habs ausprobiert, das gibt ne Warnung, aber warum?
Wenn z.B. i voher 5 ist, wie soll denn da was anderes als 6 rauskommen?
Peter
Peter Dannegger schrieb:
>> ist genauso undefiniert>> Habs ausprobiert, das gibt ne Warnung, aber warum?
Ok, das war zu kurz. Ich bezog mich dabei auf die obige Variante
i = ++i % 32,
also mit Nachverarbeitung. Dann ist nicht definiert ob
temp = i + 1;
i = temp % 32;
i = temp;
oder
i = i + 1;
i = i % 32;
gerechnet wird.
Bei i = ++i ist das "zufällig" gleich.
Peter Dannegger schrieb:
> Der GCC warnt leider gerne mal auch an Stellen, wo es garnichts zu> warnen gibt. Ich ärgere mich z.B. regelmäßig über die Forderung nach> überflüssiger Klammerung.
Ein Beispiel bitte?
Naja wenn ich meinen Code so schreibe, dass sämtliche "operator
precedence" Regeln stets zu 100% ausgenutzt werden... dann hoffe ich,
dass niemand meinen Code je lesen muss. ;-)
Peter Dannegger schrieb:
>> i = ++i;>>>> ist genauso undefiniert>> Habs ausprobiert, das gibt ne Warnung, aber warum?>> Wenn z.B. i voher 5 ist, wie soll denn da was anderes als 6 rauskommen?
Das liegt an der (bescheuerten) Definition über Sequence Points in C.
In obigem sitzt der Sequence Point im ;
Das bedeutet erst zu diesem Zeitpunk müssen alle Nebeneffekte
angeschlossen sein.
++i definiert, dass als Wert für die weitere Verwendung im Ausdruck, der
um 1 erhöhte Wert von i benutzt wird. Nebeneffekt dieser Anweisung ist
es, dass der um 1 erhöhte Wert wieder in i abgespeichert wird. Und genau
darum geht es: Der Compiler darf diesen Nebeneffekt in der
Ausführungssequenz hin und herschieben, wie es ihm passt. Er muss nur
beim Sequence Point damit fertig sein.
Auch wenn das kein Compiler machen wird, dürfte er
i = ++i + 5;
so compilieren (mit einer etwas komplizierteren Expression sieht man das
alles besser)
tmp = i + 1; ( ++i wertmässig evaluieren )
i = tmp + 5; ( ++i + 5; an i zuweisen)
i = tmp ( ++i fertig stellen, indem das Inkrement
zugewiesen wird )
Im Standard liest sich das dann so:
"Between the previous and next sequence point an object shall have its
stored value modified at most once by the evaluation of an expression.
Furthermore, the prior value shall be accessed only to determine the
value to be stored."
Auf deutsch: wird versucht zwischen 2 Sequence Points einer Variablen
2-mal ein Wert zu verpassen, hat man undefiniertes Verhalten, weil nicht
geregelt ist, welche Wertzuweisung zuerst erfolgt.
Peter Dannegger schrieb:
> Also ich hab im Web zu "undefined behavior" ausschließlich Beispiele mit> Postincrement gefunden und keines mit nur einem Preincrement.
Es ist trotzdem das gleiche Prinzip.
Ich glaub, jetzt verstehe ich es langsam.
i = ++i; ist ein Spezialfall, der immer ein eindeutiges Ergebnis hat.
Ich war bisher immer davon ausgegangen, daß rechts vor links gilt.
Also der Compiler wird zwar in der Regel die linke Seite des "=" als
letztes ausführen, ist dazu aber nicht gezwungen.
Und der ?: Operator gestattet wie || und && die bedingte Ausführung von
Ausdrücken und muß dazu den Entscheidungsausdruck komplett abgeschlossen
haben. Daher ist es eindeutig.
Peter
> Ich glaub, jetzt verstehe ich es langsam.> i = ++i; ist ein Spezialfall, der immer ein eindeutiges Ergebnis hat.
Nein. i = ++i ruft undefiniertes Verhalten hervor, da es zwischen zwei
Sequenzpunkten die Variable i zweimal modifiziert. Ob nun eins davon
eine Zuweisung ist oder nicht, spielt dabei keine Rolle. Es ist also
genauso undefiniert, wie z.B. ein ++i * ++i
Und ja, ich habe schon unterschiedliche Ergebnisse bekommen, nachdem ich
es auf verschiedenen Compilern übersetzt hatte.
> Ich war bisher immer davon ausgegangen, daß rechts vor links gilt.> Also der Compiler wird zwar in der Regel die linke Seite des "=" als> letztes ausführen, ist dazu aber nicht gezwungen.
"Undefiniertes Verhalten" heißt, daß der Compiler machen darf, was er
will. Jedes Ergebnis ist korrekt.
> Und der ?: Operator gestattet wie || und && die bedingte Ausführung> von Ausdrücken und muß dazu den Entscheidungsausdruck komplett> abgeschlossen haben. Daher ist es eindeutig.
Richtig. Deshalb gibt es bei diesen Operatoren einen Sequenzpunkt.
>Also ist nun> writer_out = ++writer_out >= TX_BUFFER_SIZE ? 0 : writer_out;>> die effizienteste Lösung, einen Ringpufferindex definiert zu> inkrementieren?
Ein einfaches:
Nur wenn es eine Zweierpotenz ist. Die gezeigte Alternative ist in
diesem Fall einen Hauch weniger effizient, bei anderen Puffergrössen
aber dramatisch effizienter.