Klausuraufgabe verstehen die 2.

Vermutung: 129 * 255 = 32895 > 32767

Ein Überlauf darf stattfinden!
Auch stumm.
Das ist im C++ Standard schon so fixiert.

Das Problem scheint zu sein, dass der Gcc hier fürchterlich aggressiv optimiert.
Beleg: Denn mit Volatile ist sofort Ende mit dem Problem.

Das einzig wirklich richtig böse, ist hier wirklich, das Versagen, ohne Meldung.
Und noch böser, dass der Fehler bis ins Serial.print weiter transportiert wird.

Ok, aber wenn der Fehler vom Compiler ignoriert wird, dann kann die Printroutine nix dafür. Die gibt nur aus, was ihr vorgesetzt wird.

Das mit volatile habe ich übersehen.

Gruß Tommy

Hallo,

genau meine Meinung wie auch schon geschrieben. Warum soll Serial etwas falsch machen? Gibt nur aus was ihm übergeben wird. Die Frage lautet doch warum der Datentyp int nicht korrekt beachtet wird. Er wird im Fehlerfall entweder im positiven Bereich abgeschnitten, halber int Wertebereich oder er wird unsigned long gewandelt, was der Compiler selbstständig nicht tun dürfte. Vorallendingen der halbe int Bereich ist komisch. Eine gcc Versionsabhängigkeit gibts auch, je nachdem wie man testet. Es bleibt kompliziert.

Vielleicht hat das Problem mit der Syntaxverschärfung zu tun die ab gcc10 Einzug hält, weil Zugriffsprobleme bekannt sind unter bestimmten Umständen. https://www.mikrocontroller.net/topic/495657

dann kann die Printroutine nix dafür. Die gibt nur aus, was ihr vorgesetzt wird.

Dass die nix dafür kann, mag sein...

Aber:
Sie bekommt eine Variable vom Type int übergeben, gibt aber 4294952760 aus, was nun überhaupt nicht in int rein passt.

Falsch addieren ist schon gruselig genug....
Dann das noch....

Vielleicht hat das Problem mit der Syntaxverschärfung zu tun die ab gcc10 Einzug hält, weil Zugriffsprobleme bekannt sind unter bestimmten Umständen. https://www.mikrocontroller.net/topic/495657

Ja, nee...

Habs mit der 7.3 9.2 und 10.x probiert.
Bei allen das stumme Versagen mit meinem ersten Beispiel.

Da müsste man mal in den generierten Code schauen, ob sie überhaupt einen int vorgesetzt bekommt.
Nicht das da durch einen Überlauf plötzlich ein ganz anderer Typ erscheint.
Aber grausam ist das Ganze schon.
Hat schon wer eine issue bei gcc auf gemacht?

Gruß Tommy

Hallo,

mir kommen noch zu viele Abhängigkeiten unters Auge. Je nachdem welchen Code man testet gibts gcc Versionsabhängigkeiten. Ich wüßte gar nicht wie ich das Problem darlegen sollte. Wenn dann würde ich vielleicht erstmal im mikrocontroller Forum fragen. Da würde erstmal die Sprachbarriere wegfallen.
Im aggressiv optimierten Sketch sehe ich im Dump gar nicht durch. Aber selbst im weniger aggressiven Dump blicke ich nicht durch mit welchen Datentyp er rechnet. result wird als int angelegt und das er mit 255 aufsummiert, den Rest erkenne ich nicht.
Der Code zum Dump.

byte x[200];
int result;

void setup()
{
  Serial.begin(115200);
  Serial.println("\nStart");

  // alle Zellen vorbesetzen
  for(byte &data:x) data = 0xFF;
  
  for(byte i=128; i<131; i++)
  {
    summieren(i);
  }
  
}

void loop()
{
}

void summieren (const byte count)
{
  // summe über alle Zellen
  result = 0;
  for(byte i=0; i<count; i++)
  {
    result = result + x[i];
  }
 
  // summe zeigen
  Serial.println(result);
}

int_falscher_Ueberlauf_d.ino.asmdump.txt (66.3 KB)

Da müsste man mal in den generierten Code schauen, ob sie überhaupt einen int vorgesetzt bekommt.

Habe bei mir geschaut:
Tja...
Es wird die Methode aufgerufen, welche für unsigned long vorgesehen ist.

Print::printNumber(unsigned long, unsigned char)
Warum auch immer...
Von mir beabsichtigt war das nicht.
Ermöglicht aber das Ergebnis.

Zu sehen ist allerdings, dass die Variable selber überhaupt nicht verwendet wird.
Nirgendwo wird result gespeichert.
Es liegt ausschließlich in Registern.

In dem Zusammenhang ist auch die Optimierung von

for(byte &data:x) data = 0xFF;
sehr interessant!
Die Schleife wird durch einen memset() Aufruf ersetzt.

Hallo,

an der Arduino Serial Print Funktion liegt es nicht. Ich habe das ohne Arduino IDE in AS7 auf einen ATmega4809 ausprobiert, dort hatte ich bevor ich MCUdude kannte einmal das Arduino Print nachgebaut. Gleicher Effekt.

Mir fiel dabei folgendes auf, egal ob AS7 oder Arduino IDE. Lasse ich in der summieren Funktion Zwischenwerte von result ausgeben stimmt alles. Ohne Zwischenwerte ist es wieder falsch. Es liegt demnach allein an der Optimierung was scheinbar einen falschen Datentyp nach sich zieht.

Mit Optimierungslevel -O0 und -O1 stimmts und ab -O2 gehts schief.

Ich bestätige das!
Die Optimierung baut Mist, ohne Meldung.

byte x[100];
byte y[100];



void printer(unsigned long value)
{
  Serial.print("unsigned long: "); Serial.println(value);
}

void printer(int value)
{
  Serial.print("int: "); Serial.println(value);
}



void setup()
{
  Serial.begin(9600);
}

void loop()
{

  // alle Zellen vorbesetzen
  for(byte &data:x) data = 0xFF;
  for(byte &data:y) data = 0xFF;
  
  int result = 0; 
  for(byte data:x) result += data;
  for(byte data:y) result += data;
  
  // summe zeigen
  printer(result);

}

Wenn es wirklich nur der Datentype wäre, der sich ändert, dann würde ich hier diese (fehlerhafte) Ausgabe erwarten:

unsigned long: 4294952760

Es erscheint aber diese Unmöglichkeit:

int: 4294952760

Das wirklich korrekte Ergebnis ist natürlich weiterhin:

int: -14536

combie:
Wenn es wirklich nur der Datentype wäre, der sich ändert, dann würde ich hier diese (fehlerhafte) Ausgabe erwarten:

Es erscheint aber diese Unmöglichkeit:

int: 4294952760
Das wirklich korrekte Ergebnis ist natürlich weiterhin:
int: -14536

Das ändert sich nur, wenn eine Operation mit result gemacht wird, die int bedingt.

// Variante 1: unsigned long a;
// Variante 2: unsigned long a;
//             int b;

byte x[100];
byte y[100];
unsigned long a;
int b;

void printer(unsigned long value)
{
 Serial.print("unsigned long: "); Serial.println(value);
}

void printer(int value)
{
 Serial.print("int: "); Serial.println(value);
}

void setup()
{
 Serial.begin(9600);
}

void loop()
{

 // alle Zellen vorbesetzen
 for(byte &data:x) data = 0xFF;
 for(byte &data:y) data = 0xFF;

 int result = 0;
 for(byte data:x) result += data;
 for(byte data:y) result += data;
 a=result;
 b=result;
 // summe zeigen
 printer(result);
 printer(a);
 printer(b);
}.

Ich hatte das gestern abend schon mal in ähnlicher Form - dadurch bin ich dann drauf gekommen, das offensichtlich in UL gewandelt wird und zur Ausgabe beide Werte aneinandergeschoben werden, was die 14536 ohne Vorzeichen in der Ausgabe erklärt.

Denn wenn result -= data; gerechnet wird, passiert das nicht, da der Compiler offesichtlich merkt, das der Wertebereich überschritten wird.

Hallo,

habe auch nochmal probiert. Mit dem Verfahren der Funktionsüberladung arbeitet Print, also auch mein Nachbau. Die Endumwandlung von Integer zu String mache ich mit ltoa bzw. ultoa. Also etwas anders wie Arduino Print. Hat jedoch den gleichen Effekt.

Ich habe meine print Funktionsüberladungen mit int32/uint32 auskommentiert. Es wird nicht gemeckert das die passende Auswahl fehlen würde. Bestätigt combies Test mit dem "int", also das der Datentyp von result bis dahin korrekt bleibt.

Mist kommt bei mir erst raus, mit höheren Optimierungslevel, wenn die Ausgabe im positiven Bereich auf ultoa trifft. Ändere ich das auf ltoa ab, was für int noch passt, dann stimmt auch wieder die Ausgabe. Das bedeutet das hierbei schon der Vergleich <0 nicht mehr stimmt. Der negative Bereich geht an dem Punkt verloren bzw. komplett schief.

    // --- Umformung und Weitergabe ----------------------------- 
    
    template <class T>
    void printInteger(const T var)
    {
        char buffer[11];
        if (var < 0) {
            ltoa(var, buffer, 10);      // ltoa
        }
        else {
            ltoa(var, buffer, 10);     // ultoa, geändert
        }
        putString(buffer);
    }

Ja, der Optimimierer, der ist es, welcher hier (hinterrücks) zuschlägt.....

Hier ein Beispiel wo man sehen kann wie lustig das ist....

byte feld[200];


// https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/Print.h

void printer(unsigned long value)
{
  // beabsichtigter Aufruf von size_t Print::println(unsigned long, int = DEC);
  Serial.print("unsigned long: "); Serial.println(value);
}

void printer(int value)
{
  // beabsichtigter Aufruf von size_t Print::println(int, int = DEC);
  Serial.print("int: "); Serial.println(value);
}



void setup()
{
  Serial.begin(9600);

    
  // alle Zellen vorbesetzen
  for(byte &data:feld) data = 0xFF;

  
  int result = 0; 
  for(byte data:feld) result += data;
  printer(result); 

  
 /*  
  result = 0; 
  for(byte data:feld) result += data;
  printer(result); 
*/

  
}

void loop(){}

So kommt das falsche Ergebnis:

int: 4294952760

Macht man die /* */ Kommentar Zeichen weg, also lässt die Additionsschleife 2 mal laufen, kommt beide mal das richtige

int: -14536
int: -14536

Selbst, wenn man den ganzen Klumpen:

 /*  
  result = 0; 
  for(byte data:feld) result += data;
  printer(result); 
*/

ersetzt durch ein:printer(4711); // kontrollausgabe
kommt das richtige Ergebnis!

int: -14536
int: 4711

Eine Kontrolle im AsmCode zeigt, dass die Addition unverändert bleibt, aber Print::println() nicht mehr inline eingebaut wird.
Der Optimizer baut den Mist also beim Inlining, und nicht wirklich beim rechnen.

Hallo,

das schafft richtig Vertrauen :confused: Der Pentium Bug erscheint damit in einem völlig anderem Licht.

Wenn dann würde ich vielleicht erstmal im mikrocontroller Forum fragen.

Done.
Mal schauen, ob es Haue gibt.

Wir stehen geschlossen hinter dir. :slight_smile: (Ich hätte es in der Compiler Rubrik gepostet, höhere Chancen auf Experten.)

Abschlussbericht in Kurzfassung:

Punkt 1: Alles ist ok!

Der Kompiler ist in Ordnung.
Er hält sich an der der Stelle an den C++ Standard, wie es sich gehört.
Arduino und sein AVR Core ist auch voll ok.

Punkt 2: Die Aufgabe im Eingangsposting ist defekt.

Es sollen byte addiert werden, soweit ok.
Das Ergebnis soll in einem int landen, da ist der Haken.

Der Sprachstandard bezeichnet den int Überlauf als undefiniert.
Also ist dem Kompiler auch an der Stelle ein undefiniertes Verhalten erlaubt.
Und sowas zeigt sich ja auch, sogar ein überraschendes Verhalten.

Die Berechnung erfolgt nach den Regeln des AVR völlig korrekt. Es werden negative Zahlen berechnet, bzw ergeben sich die korrekten Ergebnisse nach den Regeln des Zweierkomplements.

Allerdings ist der Kompiler der Überzeugung, dass das Ergebnis niemals kleiner 0 sein kann.
(hätte meine damalige Mathe Lehrerin auch für richtig befunden)

Da die Addition von positiven Werten niemals negativ werden kann, darf der Kompiler alle Abhandlungen für negative Zahlen an der Stelle weg optimieren.
Genau das tut er!

Darum wird hier Print::print(long n, int base) alles, was mit negativen Zahlen zu tun hat, gnadenlos entsorgt.

Die Zahl wird so ausgegeben, wie sie als positive long Zahl aussehen würde.

Tata!
Problem erkannt: Aufgabe kaputt.
Alles ist gut.

Wer es gerne möchte, kann dem Kompiler auch über den Parameter -fwrapv mitteilen, dass ein Wraparound stattfindet, bzw. stattfinden kann.
Dann ist die Welt wieder in Ordnung.

Alternativ, folgendes Pragma vor der betreffenden Funktion unterbringen

#pragma GCC optimize ("-fwrapv")

evtl. garniert mit einem Satz push+pop Pragmas.

Schlusswort:
Vorsicht mit dem Vorzeichen behaftetem Überlauf!
Er erzeugt ein undefiniertes Verhalten.
Ebenso Vorsicht mit impliziten und expliziten Casts von negativen Zahlen zu vorzeichenlosen.

Am besten: Meiden, wie der Teufel das Weihwasser.

Link

Danke für die ausführliche Klarstellung und die Warnungen.

Gruß Tommy

Hallo,

ich möchte das hier nicht ausweiten. Aber das hier wie dort mit Matheregeln zu begründen halte ich einfach für falsch. Mathematisch falsch ist auch ein unsigned Überlauf auf 0. Nur das das eben genauso so implementiert ist. Nur mal so als Gedanke.

Ja...
Der Haken ist eben, dass unsigned Überläufe im C++ Standard wohl definiert sind.
Signed Überläufe eben nicht, bzw. explizit als undefined gelten

Bei einem signed Überlauf darf der Kompiler auch einkaufen gehen, oder Radieschen anbauen.

Wenn einem das nicht schmeckt, wird man als erstes die Sprachdefinition ändern müssen und danach alle Kompiler.

Ich bin mir nicht ganz sicher, aber vermute, dass du damit auf taube Ohren stoßen wirst.
Eben auch, weil es viele Optimierungen unterbinden würde.

Hallo,

ja. :confused: Da ich ein viel zu kleines Licht bin brauch ich an das Gremium oder wie man das nennt des Sprachstandards und gcc gar erst herantreten mit einer Bitte um korrekte Implementierung. Ich habe schon weiter gelesen und kann sagen, dass wird niemals geändert, weil das auf Grund der besseren Optimierungsmöglichkeit genauso gewollt ist, dieses UB. Mit dem Nachteil müssen dann alle leben. Das heißt, egal wer da anfragen würde, er bekäme die Antwort das es wegen der Optimierung genauso bleibt, weil es damals bewußt so definiert wurde.

Es gab scheinbar vor Jahren auch eine Option -fno-strict-overflow, (zeitgleich mit -fwrapv) die scheinbar gestrichen wurde oder durch andere ersetzt wurde. Die diente nur dafür den Compiler zu sagen den Überlauf nicht wegzuoptimieren und erzeugte damals minimal weniger Code im Vergleich zu -fwrapv. Wir reden hier von Informationen von vor 2013.