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
intmain(void)
2
{
3
uart_init();
4
5
int8_tn=-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
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.
>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:
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.
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.
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?
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.
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.
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.
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.
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
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
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.
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).
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:
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
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.
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.
:-)
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.
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.
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.
>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:
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.
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?
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.
> 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...
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.
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.
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.
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.
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.
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.
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.
--