Forum: Compiler & IDEs int8_t deklariert, int16_t bekommen? (AVR Studio 5)


von Reinhard R. (reinhardr)


Lesenswert?

Hallo,

ich habe ein sonderbares Problem. Wenn ich in einem einfachen Programm 
eine 8bit Variable inkrementiere kann es passieren dass sie erst an der 
16bit Grenze (32767) überläuft. Außerdem wird sie auch als 16bit 
Variable interpretiert. Bizarrerweise passiert das wenn die Variable mit 
dem Operator "++" inkrementiert wird, aber nicht wenn ich 1 addiere.
1
int main(void)
2
{
3
    uart_init();
4
5
    int8_t n = -100;
6
7
    while(1)
8
    {
9
        itoa(n, buffer, 10);
10
        uart_puts(buffer);
11
        uart_puts("\r\n");
12
        n++;    //Überlauf bei 32767
13
        //n+=1; //Überlauf bei 127
14
    }
15
}

Gibt es eine Erklärung dafür im C Standard bzw. der stdlib? Gelegentlich 
macht es ja Sinn gezielt mit Überläufen zu arbeiten. Wie kann man in so 
einem Fall die korrekt Bitbreite erzwingen?

Das Programm wurde mit AVR Studio 5 (avr-gcc 4.5.1) übersetzt und läuft 
auf einem ATMega16. Die UART Funktionen entstammen dem Tutorial.

Gruß,
Reinhard

: Verschoben durch Moderator
von Stefan E. (sternst)


Lesenswert?

Reinhard R. schrieb:
> Gibt es eine Erklärung dafür im C Standard bzw. der stdlib? Gelegentlich
> macht es ja Sinn gezielt mit Überläufen zu arbeiten.

Überläufe für vorzeichenbehaftete Typen sind laut Standard undefiniert.

von Matthias L. (Gast)


Lesenswert?

>Wie kann man in so einem Fall die korrekt Bitbreite erzwingen?

Ich vermute, es liegt am itoa. Das will einen int16_t haben. Ich weiss 
nicht, was dann das tut:
1
int8_t n = ...
2
...
3
4
itoa( *n*, ...

Hast du es mal so versucht:
1
itoa( (int16_t) n, ...

von Stefan E. (sternst)


Lesenswert?

Matthias Lipinsky schrieb:
> Hast du es mal so versucht:

Es ist unsinnig an dem Code "herumzudoktern". Ein Überlauf für n ist 
undefiniert, d.h. danach ist alles möglich und erlaubt.

von Peter II (Gast)


Lesenswert?

Stefan Ernst schrieb:
> Es ist unsinnig an dem Code "herumzudoktern". Ein Überlauf für n ist
> undefiniert, d.h. danach ist alles möglich und erlaubt.

aber es darf auf jeden Fall bei

 int8_t n = -100;
 itoa(n, buffer, 10);

ein buffer ein wert > 255 stehen. Egal was sonst mit n angestellt wird.

von Stefan E. (sternst)


Lesenswert?

Peter II schrieb:
> aber es darf auf jeden Fall bei
>
>  int8_t n = -100;
>  itoa(n, buffer, 10);
>
> ein buffer ein wert > 255 stehen. Egal was sonst mit n angestellt wird.

Hä?
Also ich denke mal, da sollte irgendwo ein "kein" (oder so) stehen.
Wo wird denn behauptet, dass es so ist?

von Peter II (Gast)


Lesenswert?

Stefan Ernst schrieb:
> Hä?
> Also ich denke mal, da sollte irgendwo ein "kein" (oder so) stehen.
ja

> Wo wird denn behauptet, dass es so ist?

> itoa(n, buffer, 10);
> uart_puts(buffer);
> uart_puts("\r\n");
>  n++;    //Überlauf bei 32767
>  //n+=1; //Überlauf bei 127
ich lese das beispiel so, das er per uart den überlauf beobachtet hat.

von Stefan E. (sternst)


Lesenswert?

Peter II schrieb:
> ich lese das beispiel so, das er per uart den überlauf beobachtet hat.

Ja, aber der Wert ist dann erst nach dem "Überlauf" größer (und nicht 
wie dein Posting suggeriert direkt nach der Initialisierung). Und das 
ist erlaubt. Wenn man was macht, was undefiniert ist, ist danach alles 
erlaubt, und das "alles" ist wörtlich zu nehmen.

von Peter II (Gast)


Lesenswert?

Stefan Ernst schrieb:
> Ja, aber der Wert ist dann erst nach dem "Überlauf" größer (und nicht
> wie dein Posting suggeriert direkt nach der Initialisierung). Und das
> ist erlaubt. Wenn man was macht, was undefiniert ist, ist danach alles
> erlaubt, und das "alles" ist wörtlich zu nehmen.

aber es kann nicht sein das in einer int8_t variable ein wert der > 255 
ist drin ist.

Denn hier

itoa(n, buffer, 10);

macht der compiler aus dem int8_t ein int - das ist klar definiert und 
auch zulässig. Woher sollen denn die bits für werte > 255 kommen?




Kann jemand das beispiel überhaupt bestätigen, ich glaube es noch nicht.

von Stefan E. (sternst)


Lesenswert?

Peter II schrieb:
> aber es kann nicht sein das in einer int8_t variable ein wert der > 255
> ist drin ist.

Doch. Nochmal: "alles" bedeutet auch wirklich "alles".

Peter II schrieb:
> Woher sollen denn die bits für werte > 255 kommen?

Irrelevant. Der Compiler hat halt Code erzeugt, der davon ausgeht, dass 
es nie zu einem Überlauf kommt, und der dann dieses Symptom zeigt, wenn 
n bei INT8_MAX dann doch (unerlaubter weise) erhöht wird.

von Peter II (Gast)


Lesenswert?

@Reinhard R.

kannst du mal die lss Datei mit dem ASM-Code schicken?

von Reinhard R. (reinhardr)


Lesenswert?

Danke für die Info mit dem undefinierten Überlauf. Was mir jetzt Sorgen 
bereitet sind 2 Sachen:
A) Offensichtlich wird "irgendwo" das high byte gespeichert. Kann dabei 
eine andere Variable überschrieben werden oder brauchen int8_t in 
Wirklichkeit 2 Bytes an Speicherplatz? Oder geschieht das aufgrund von 
Compileroptimierungen in irgendwelchen Registern und das Ergebnis wird 
in komplexeren Programmen unvorhersehbar?
B) itoa bekommt ein Argument mit einem kleineren Wertebereich als 
deklariert. Das automatisch und korrekt zu erweitern sollte ja kein 
Problem sein (im Gegensatz zum umgekehrten Fall). Die Funktion holt sich 
aber auch wieder "irgendwo" ein high byte her.

Noch eine Anmerkung, das Programm läuft wie erwartet bei -100 los, was 
auch im Wertbereich des int8_t liegt. Ein Start bei -1000 ist, wie zu 
erwarten, nicht möglich (wird beschnitten) aber nach dem Überlauf wird 
der Wert erreicht.

Mit dem Typ uint8_t und dem Startwert 0 läuft alles problemlos.

@Peter II,
lss Datei kommt gleich.

Gruß
Reinhard

von Peter II (Gast)


Lesenswert?

Reinhard R. schrieb:
> Offensichtlich wird "irgendwo" das high byte gespeichert. Kann dabei
> eine andere Variable überschrieben werden
nein, das sollte nicht passieren

> oder brauchen int8_t in
> Wirklichkeit 2 Bytes an Speicherplatz?
100% nein,

> Oder geschieht das aufgrund von
> Compileroptimierungen in irgendwelchen Registern
das ist meine vermutung das er eine int addition macht und das High 
Register dann gleich für atoi verwenden. Wenn man etwas code zwischen 
n++ und atoi schreibt dann sollte dieser effekt weg sein.

> und das Ergebnis wird in komplexeren Programmen unvorhersehbar?
das auf jeden Fall

von Knut (Gast)


Lesenswert?

Was passiert, wenn du die Variable als
1
static
 deklarierst?

Probier das mal.


Knut

von Reinhard R. (reinhardr)


Angehängte Dateien:

Lesenswert?

Hier die .lss Datei. Mal sehen, vielleicht werde ich ja selbst auch 
daraus schlau (habe sehr wenig Erfahrung mit Assembler).

von Peter II (Gast)


Lesenswert?

sieht für mich so aus als ob er aus dem int_8 ein uint_16 gemacht hat 
und intern alles mit 16bit rechnet. (R16, R17).

Die Variable hat scheinbar nicht mal platz auf dem Heap bekommen, sie 
lebt nur in einem Register.

von Reinhard R. (reinhardr)


Angehängte Dateien:

Lesenswert?

Knut schrieb:
> Was passiert, wenn du die Variable als
1
static
 deklarierst?
>
> Probier das mal.
>
>
> Knut

Mit static und volatile funktioniert es ohne dass der Wertebereich 
verlassen wird, und an der Stelle an der man es erwarten würde (127 => 
-128).

von Stefan E. (sternst)


Lesenswert?

Reinhard R. schrieb:
> Mit static und volatile funktioniert es ohne dass der Wertebereich
> verlassen wird, und an der Stelle an der man es erwarten würde (127 =>
> -128).

Und?
Es ist unsinnig an den Symptomen herumzudoktern, "undefiniertes 
Verhalten" bleibt "undefiniertes Verhalten". Code mit undefinierten 
Verhalten zu verwenden, nur weil dieses undefinierte Verhalten zufällig 
dem entspricht, was man gerne hätte, ist eine verdammt schlechte Idee. 
Die einzig korrekte Lösung wäre es, den Code so zu verändern, dass er 
ein definiertes Verhalten hat. Und wenn du einen Signed-Overflow haben 
möchtest, bedeutet das, dass du den selber machen musst:
1
if (n == INT8_MAX)
2
    n = INT8_MIN;
3
else
4
    n++;

von Reinhard R. (reinhardr)


Lesenswert?

Die letzte Bemerkung bezog sich nur auf meine intuitive Vorstellung wie 
ich mir den Überlauf vorstelle. Ich wollte damit nicht andeuten dass die 
Ergebnisse ein paar kleiner Tests mit einer bestimmten Compilerversion 
allgemeine Gültigkeit haben. Sie können aber verstehen helfen was eben 
genau dieser Compiler macht. Interessanter wäre eher das Warum?

Womit ich keine Freude habe, ist dass Werte außerhalb des Wertebereichs 
eines 8bit Integers auftauchen, wenn ich mit einem ebensolchen arbeite.

Gruß,
Reinhard

von Martin (Gast)


Lesenswert?

Stefan Ernst schrieb:
> Es ist unsinnig an den Symptomen herumzudoktern, "undefiniertes
> Verhalten" bleibt "undefiniertes Verhalten".

Word.

von Schlonz (Gast)


Lesenswert?

Ok, nach Standard ist das Verhalten bei einem signed integer overflow 
undefiniert, und undefiniert kann alles heißen. Ich habe das bisher 
allerdings auch immer so verstanden, dass der int8_t im Nachhinein einen 
beliebigen Wert haben kann. Allerdings innerhalb des Wertebereichs eines 
int8_t.

Mit dem hier vorgeführten Verhalten hätte ich nicht gerechnet.

Tatsächlich wird bei dem Aufruf der Funktion itoa() der int8_t implizit 
in einen int16_t gecastet. Und es ist schon bemerkenswert, dass in 
dieses Ergebnis mehr als 8 bit eingehen.

Denn der Compiler erzeugt ja vermutlich den gleichen Code, egal ob ein 
Überlauf während der Laufzeit nun tatsächlich stattfindet oder nicht. 
Ok, das wurde noch nicht verifiziert, ich nehme das einfach mal an. Das 
heißt doch, dass der Compiler hier unnötigen 16-Bit-Code erzeugt oder 
zumindest beim Cast von int8_t nach int16_t mehr Speicherzellen 
anschaut, als er eigentlich "dürfte".

Hmm, komme gerade ins Grübeln.

von Floh (Gast)


Lesenswert?

Schlonz schrieb:
> Mit dem hier vorgeführten Verhalten hätte ich nicht gerechnet.

Wäre interessant, die Variable mal global oder eben volatile anzulegen. 
Dann müsste der Wertebereich eigentlich in 8bit bleiben.
Aktuell wird die lokale Variable ja in 2 REgistern abgelegt, dort 
verarbeitet und auch von dort zur Ausgabe ausgelesen, nach dem Motto, 
warum casten, wenn die Berechnung schon in 16bit abläuft.
:-)

von (prx) A. K. (prx)


Lesenswert?

Denk beim Grübeln daran, dass der GCC zu 8-Bit Prozessoren ein eher 
gespanntes Verhältnis hat. Er geht - wie die Sprache C per Definition - 
oft davon aus, dass ein "int", hier 16 Bits breit, die effizienteste 
Wortgrösse der Maschine ist. Das ist bei AVRs nicht der Fall, was 
manchmal zum Nachteil des erzeugten Codes ausfällt.

von Schlonz (Gast)


Lesenswert?

Ok, läuft hier eine Optimierung ab nach dem Motto:

"Ich muss ohnehin für den Aufruf von itoa() casten, also lege ich die 
Variable von Anfang an als 16-Bit-Integer an. Das darf ich machen, weil 
ich mich als Compiler bei einem signed int nicht um Überläuft kümmern 
muss, denn der Standard erlaubt mir hier undefiniertes Verhalten".

Ein Cast von int8_t auf int16_t ist auf Assembler-Ebene tatsächlich 
nicht trivial und könnte diese Optimierung begründen.

von Schlonz (Gast)


Lesenswert?

P.S.: Ich kann mich trotzdem nur schwer mit dem Verhalten anfreunden, 
dass ein int8_t auf einmal größer als 127 wird.

von (prx) A. K. (prx)


Lesenswert?

Dummerweise hat der Compiler die besseren Argumente auf seiner Seite. 
Deine Gefühle sind ihm egal. Daher wirst du wohl daran gewöhnen müssen, 
korrekten Code zu schreiben.

von Matthias L. (Gast)


Lesenswert?

>Ein Cast von int8_t auf int16_t ist auf Assembler-Ebene tatsächlich
>nicht trivial und könnte diese Optimierung begründen.

WIeso nicht? Es muss nur das höchste Bit (also das 7.) in das neue 
höchste Byte kopiert werden.

Also etwa so: In Rx soll die int8_t stehen, und Ry soll das Highbyte 
werden:
1
CLR Ry
2
SBRC Rx, 7
3
SER Ry

von (prx) A. K. (prx)


Lesenswert?

Matthias Lipinsky schrieb:

> WIeso nicht? Es muss nur das höchste Bit (also das 7.) in das neue
> höchste Byte kopiert werden.

Eben. Eine 16-Bit Inkrementierung ist einfacher als eine 
Vorzeichenerweiterung.

von Matthias L. (Gast)


Lesenswert?

>Eben. Eine 16-Bit Inkrementierung ist einfacher als eine
>Vorzeichenerweiterung.

Das stimmt natürlich. Da reicht ein ADIW.

von Martin (Gast)


Lesenswert?

Schlonz schrieb:
> Mit dem hier vorgeführten Verhalten hätte ich nicht gerechnet.

Es gab eine gcc-Version, die hat Emacs mit Türme von Hanoi gestartet. 
Hättest du damit gerechnet?

von (prx) A. K. (prx)


Lesenswert?

Viel interessanter finde ich die Frage, wieso der Compiler n+=1 anders 
behandelt als n++.

von Schlonz (Gast)


Lesenswert?

Ich verstehe nun, warum gcc das tut, was er tut.

Hätte man das in Assembler per Hand geschrieben, hätte man natürlich 
eine 8-bit-Version von itoa gebastelt. Unter den gegebenen Umständen 
(nur 16-Bit-itoa verfügbar) hat gcc die optimale Variante gewählt, 
nämlich von Anfang an alles mit 16 Bit gerechnet, weil das 16-bittige 
Inkrementieren weniger Aufwand ist als ein Cast von 8 bit auf 16 bit 
(signed) in jedem Schleifendurchlauf.

Damit kann ich jetzt ruhig schlafen. Ich habe mir allerdings noch einmal 
hinter die Ohren geschrieben, "undefiniertes Verhalten" nicht nur auf 
den begrenzten Kontext einer Variable zu beziehen, wie fälschlicherweise 
bisher.

von Schlonz (Gast)


Lesenswert?

> Viel interessanter finde ich die Frage, wieso der Compiler n+=1 anders
> behandelt als n++.

In der Tat. Vielleicht stößt ja Jörg Wunsch bald auf diesen Thread...

von (prx) A. K. (prx)


Lesenswert?

Das dürfte eher Johanns Gebiet sein.

von Peter II (Gast)


Lesenswert?

Martin schrieb:
> Es gab eine gcc-Version, die hat Emacs mit Türme von Hanoi gestartet.
ich kenn das nur als VI-Makro.

von Läubi .. (laeubi) Benutzerseite


Lesenswert?

A. K. schrieb:
> Viel interessanter finde ich die Frage, wieso der Compiler n+=1 anders
> behandelt als n++.

Ich würde folgendes vermuten:

[n++] --> Explizites increment des types (signed int_8)
[n += 1] --> [n = n + (int16)1] --> n wird auf int16 erweitert, 1 
addiert und danach auf int8 gecastet, eventuell wird daher auch die 
16bit Optimierung nicht durchgeführt.

von J.T.Kork (Gast)


Lesenswert?

... wie sieht denn das typedef zu int8_t aus?

Etwas "int8" zu nennen ist einfachj.

von (prx) A. K. (prx)


Lesenswert?

Läubi .. schrieb:

> Ich würde folgendes vermuten:

Nette Idee, nur ist das beschriebene Verhalten genau andersrum. ;-)

von (prx) A. K. (prx)


Lesenswert?

Ok, hab mal reingesehen. Es handelt sich bei der n++ Variante um eine 
Optimierung von Schleifenzählern (ivopts => induction variable 
optimizations).

Tatsächlich wird im erzeugten Code "n" überhaupt nicht inkrementiert, 
sondern es wird ein zusätzlicher 16-Bit Zähler ab 0 hochgezählt und in 
der Schleife zu der auf 16 Bit erweiterten Version von "n" addiert. Die 
Erweiterung von "n" ist schleifeninvariant, fliegt daher aus der 
Schleife raus.

Entsprechend der oben genannten Regel sieht der Compiler keinen Grund, 
diesen Schleifenzähler mit weniger als "int" zu implementieren.

Optimal ist das in diesem Fall freilich nicht, weil die ganze Addiererei 
3-4 überflüssige Befehle einbringt. Dass der Code in der Schleife 
dennoch kürzer ist, ist eher Zufall (und ist es auch nur dank MOVW).

Diese Optimierung findet bei -O noch nicht statt, nur ab -Os/-O2 und 
lässt sich mit -fno-ivopts abschalten.

von Rolf Magnus (Gast)


Lesenswert?

A. K. schrieb:
> Optimal ist das in diesem Fall freilich nicht, weil die ganze Addiererei
> 4 überflüssige Befehle einbringt.

Da kommt wohl zum Tragen, daß gcc eigentlich nicht für 
8-Bit-Prozessoren, oder allgemeiner für Prozessoren, deren native 
Bitbreite kleiner als die von int ist, vorgesehen war.

von (prx) A. K. (prx)


Lesenswert?

Apropos Optimierung: Wenn "n" nicht als int8_t sondern als int 
deklariert wird, dann ist der Code optimal, ist erheblich effizienter. 
Sowas kann einem öfter passieren, nicht immer sind 8-Bit Daten 
effizienter.

von (prx) A. K. (prx)


Lesenswert?

Rolf Magnus schrieb:

> Da kommt wohl zum Tragen, daß gcc eigentlich nicht für
> 8-Bit-Prozessoren, oder allgemeiner für Prozessoren, deren native
> Bitbreite kleiner als die von int ist, vorgesehen war.

Nicht wirklich, denn durch den Aufruf muss sowieso auf 16 Bits erweitert 
werden. Eine reine 8 Bit Verarbeitung kommt also nicht in Frage, selbst 
wenn der Compiler für Zwerge optimiert ist.

Wenn man den 16-Bit Parameter durch einen 8-Bit Parameter ersetzt, dann 
verfliegt der Spuk und die Verarbeitung erfolgt in 8 Bits ohne 
künstlichem Schleifenzähler.

Allerdings nehme ich an, dass nur Compiler der Komplexitätsklasse eines 
GCC überhaupt solche Optimierungen durchführen - es sei denn, die 
Zielmaschine hat besonders effiziente Schleifenbefehle, die dann genutzt 
werden können.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Reinhard R. schrieb:

> Gibt es eine Erklärung dafür [...]?

GCC-Schalter -f[no-]tree-loop-optimize

Passt übrigens besser ins GCC-Forum das.

von Rolf Magnus (Gast)


Lesenswert?

A. K. schrieb:
> Rolf Magnus schrieb:
>
>> Da kommt wohl zum Tragen, daß gcc eigentlich nicht für
>> 8-Bit-Prozessoren, oder allgemeiner für Prozessoren, deren native
>> Bitbreite kleiner als die von int ist, vorgesehen war.
>
> Nicht wirklich, denn durch den Aufruf muss sowieso auf 16 Bits erweitert
> werden. Eine reine 8 Bit Verarbeitung kommt also nicht in Frage, selbst
> wenn der Compiler für Zwerge optimiert ist.

Ja, aber man müßte die Rechnungen nicht in 16 Bit machen. Das schreibst 
du doch selber schon:

A. K. schrieb:
> Optimal ist das in diesem Fall freilich nicht, weil die ganze Addiererei
> 3-4 überflüssige Befehle einbringt.

von Knut (Gast)


Lesenswert?

Mal so ne Frage, ist den das AVR Studio5 jetzt soweit das man damit 
jetzt arbeiten kann bzw. sind alle Bugs behoben?



Knut

von der A. (the_a)


Lesenswert?

zu der ganzen Thematik habe ich mal ne Frage; ich wollte einen 
optimierten c-Code für einen Ringbuffer von 256 Bytes schreiben. Dabei 
benutze ich uint8_t als offset auf den Basiswert.
WEnn ihr jetzt erzählt der gcc verändert selbständig die 
Variablenbreiten, wie kann ich dann sicher sein, dass mein Code auch bei 
0xFF + 1 bei 0x00 weitermacht?

Danke, Adib.
--

von (prx) A. K. (prx)


Lesenswert?

Bei vorzeichenlosen Typen ist das Modulo-Verhalten bei Überlauf ganz 
offiziell definiert. Nur eben nicht bei vorzeichenbehafteten Typen.

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.