Forum: Compiler & IDEs avr-gcc: implizite Wertebereichserweiterung bei Int-Operationen


von Stephan (Gast)


Lesenswert?

Hallo zusammen,

ich frag mich grad wie der Compiler (Avrstudio5, avr-gcc) Integer-Werte 
unterschiedlicher Dimension verarbeitet.
Derzeit caste ich alles auf die maximale Größe.
Beispiel:

#define TIMER_FREQ      (uint16_t) 32768
volatile uint16_t v16 = 0;
volatile uint32_t v32 = 0;
...
v16 = [100...1023];
v32 = 256*60*TIMER_FREQ*10 / (v16*4*2);

Anmerkung: 256*60*TIMER_FREQ*10 überschreitet uint_32_t

Ich hoffe, dass der Compiler die Konstanten in Zähler/Nenner zuverlässig 
selber rauskürzt und die Rechnung auch zuverlässig automatisch auf 
32-Bit erweitert. Hab aber das ungute Gefühl, dass da manchmal/immer auf 
8/16-Bit abgeschnitten wird.

Was muss wirklich sein (casts) und was ist überflüssig? Auch für 
künftige Compilerversionen (ohne Bugs).
Best practice?


Stephan

: Verschoben durch Moderator
von Karl H. (kbuchegg)


Lesenswert?

Stephan schrieb:
> Hallo zusammen,
>
> ich frag mich grad wie der Compiler (Avrstudio5, avr-gcc) Integer-Werte
> unterschiedlicher Dimension verarbeitet.

Genau so, wie es die C-Regeln vorschreiben.

> Ich hoffe, dass der Compiler die Konstanten in Zähler/Nenner zuverlässig
> selber rauskürzt

Wenn das den Unterschied zwischen einem Overflow und keinem Overflow 
ausmacht, dann hat der Compiler einen Fehler gemacht.
Denn nach einer derartigen Optimierung muss EXAKT das gleiche rauskommen 
(innerhalb dessen, was durch die Sprache definiert wird), wie ohne 
Optimierung. Und das beinhaltet auch Overflows.

> Was muss wirklich sein (casts)

Geh deine Berechnung durch, so wie sie abgearbeitet wird. Setze deine 
Zahlenwerte ein und sieh nach welcher Teilschritt überläuft.


Edit: Allerdings kann das Umstellen einer Formel durch den Programmierer 
oft den Unterschied zwischen Overflow und keinem Overflow ausmachen.

von Marco M. (marco_m)


Lesenswert?

Karl Heinz Buchegger schrieb:
> Stephan schrieb:

>> Ich hoffe, dass der Compiler die Konstanten in Zähler/Nenner zuverlässig
>> selber rauskürzt
>
> Wenn das den Unterschied zwischen einem Overflow und keinem Overflow
> ausmacht, dann hat der Compiler einen Fehler gemacht.
> Denn nach einer derartigen Optimierung muss EXAKT das gleiche rauskommen
> (innerhalb dessen, was durch die Sprache definiert wird), wie ohne
> Optimierung. Und das beinhaltet auch Overflows.

Da Integer-Overflows undefiniertes Verhalten darstellen, kann der 
Compiler bei der Optimierung einfach so tun, als ob der Überlauf nicht 
stattfindet. Es darf also was anderes (oder sogar beliebiges) dabei 
herauskommen. Z.b. macht gcc aus sowas:
1
int wrap(int a) {
2
  return (a + 1 > a);
3
}

Das hier:
1
int wrap(int a) {
2
  return 1;
3
}

wenn Optimierung >=O2 oder Os an ist, weil der Optimierer dann so tut, 
als könne a nicht überlaufen. (die Option -fstrict-overflow ist dann an)

Die Rechnung des OP findet aber tatsächlich in unsigned statt, wo es 
keinen Überlauf sondern nur wrap-around gibt, da es sich um 
Modulo-Arithmetik handelt. Hier kann der Compiler nicht automatisch 
kürzen wie man es ansonsten erwarten würde, weil die Kürzungsregeln für 
Kongruenzen völlig anders sind. Wenn ich modulo 10 rechne, dann ist 
2x5/5 nicht 2 sondern 0, da man der Ausdruck (2x5)/5 enspricht und 2x5 
== 0 mod 10 ist.

Jetzt ist aber TIMER_FREQ ein uint16_t. Da bei avr-gcc die ints 
ebenfalls 16 Bit sind, wird bei der Auswertung von 
'256*60*TIMER_FREQ*10' der Teilausdruck (256*60) (der int ist) bei der 
Multiplikation mit TIMER_FREQ in uint16_t konvertiert. Im Grunde wird 
also 250u*60u*32768u*10u ausgerechnet, was mod 2^16 aber 0 ist:

250u*60u*32768u*10u
=> 250u*60u*5u*(2u*32768u)
=> 250u*60u*5u*(0u)
=> 0u

der Compiler sollte bei aktiver Optimierung den Ausdruck durch die 
Konstante 0 ersetzen können (Division duch 0 ist auch undefiniertes 
Verhalten und das kann da auch ignoriert werden)

Und tatsächlich wird:
1
uint32_t foo()
2
{
3
  return 256*60*TIMER_FREQ*10 / (v16*4u*2u);
4
}

zu:
1
foo:
2
/* prologue: function */
3
/* frame size = 0 */
4
/* stack size = 0 */
5
.L__stack_usage = 0
6
  lds r24,v16
7
  lds r25,v16+1
8
  ldi r22,lo8(0)
9
  ldi r23,hi8(0)
10
  ldi r24,hlo8(0)
11
  ldi r25,hhi8(0)
12
/* epilogue start */
13
  ret

also: return 0;

Auf einer Maschine mit 32-bit ints sieht das wieder anders aus, weil bei 
der Muplitplikation zwischen int und uint16_t ein int rauskommt. Der 
Compiler könnte jetzt tatsächlich den Ausdruck kürzen und das macht der 
auch in der Regel. Allerdings funktioniert es bei gcc (und clang) nicht, 
wenn der Zähler-Ausdruck bereits einen Überlauf hat, weil der Optimierer 
den kompletten Ausdrucck nicht sieht und mit einem Überlauf-Wert 
rechnet.

von Stephan (Gast)


Lesenswert?

Danke für die ausführlichen Infos.

Dann müssen die Casts wohl bleiben... Also:
1
v32 = 256ul x 60ul x (uint32_t)TIMER_FREQ x 10ul / (v16 x 4u x 2u);
wobei ich hier bei 4u und 2u das "u" auch weglassen könnte.

Wenn ich sicher sein will, dass die Faktoren 4 und 2 im Nenner vom 
Compiler mit den Konstanten im Zähler zu einer Konstante zusammgefasst 
werden muss ich das wohl auch genau so hinschreiben.

Hatt halt irgendwie gehoft, letztenendes kann der Compiler aber nicht 
wissen ob mir eine kleine Ungenauigkeit bei der Konstante auch taugt, 
oder ob ich einen Wrap-Around haben will ohne das explizit mit 
Modulo-Operator hinzuschreiben.
Jeder Mensch würde wohl sagen 32-Bit-Ergebnis, Wert im Zähler kleiner 
als 32 Bit, im Nenner weniger als 16 Bit, das soll mit 32 Bit gerechnet 
werden.


Stephan

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


Lesenswert?

Stephan schrieb:
> (uint32_t)TIMER_FREQ

Der Typecast ist nicht nötig, da die Operanden davor ja durch den
Suffix "ul" bereits bewirken, dass der Ausdruck vom Typ "unsigned
long" ist.

Ich hoffe mal, die "x" sollen "*" sein. ;)

von Stephan (Gast)


Lesenswert?

Jörg Wunsch schrieb:
> Der Typecast ist nicht nötig, da die Operanden davor ja durch den
> Suffix "ul" bereits bewirken, dass der Ausdruck vom Typ "unsigned
> long" ist.

Hatte ich auch mit 99% Sicherheit so verstanden.
Ich programmier MC nur sporadisch. Da kommts mir letztenendes 
transparenter vor das überall explizit hinzuschreiben.
Aus den 256ul eine Compiler-Konstate gemacht (ul nicht mehr erkennbar), 
Formel umgestellt und schon gehts nicht mehr.

Macht der zusätzliche Cast von der Geschwindigkeit was aus (auch wenn 
TIMER_FREQ eine uint16_t-Variable wäre)?
Theoretisch ist ja klar, dass in (uint32_t)TIMER_FREQ nur die unteren 16 
Bit interessieren können.
Eine 32*16-Bit-Multiplikation wäre also ausreichend.

Jörg Wunsch schrieb:
> Ich hoffe mal, die "x" sollen "*" sein. ;)

Ja. Das letzte mal hats mir nur " * " irgendwie verwürfelt. Ohne 
Leerzeichen wars mir aber zu unübersichtlich.

Stephan

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.