Rechnung gibt falsches Ergebnis.



  • @Finnegan sagte in Rechnung gibt falsches Ergebnis.:

    Auch interessant:

    #include<iostream>
    #include<math.h>
    
    using namespace std;
    
    int main()
    {
        cout << boolalpha << ((486 - pow(3, 5)) == (486 - pow(3, 5))) << "\n";
    }
    

    MinGW.org: false, MinGW-w64: true
    Alles mit -O0.

    DAS macht mir Angst!?



  • @Swordfish sagte in Rechnung gibt falsches Ergebnis.:

    MinGW.org: false, MinGW-w64: true
    Alles mit -O0.

    DAS macht mir Angst!?

    Ja, wenn ich nicht vorher schon was wichtiges übersehen hätte, hätte ich auch laut "Compiler/Standardbibliothek-Bug!" geschrieen 😉

    Da gibt es aber nicht viel dran zu rütteln. Ich vermute, dass die pow-Implementierung irgendwie zustandsbehaftet ist - vielleicht eine gobale Variable oder FPU-Flags, die nicht korrekt wiederhergestellt werden.

    Ich vermute es ist eher die pow-Implementierung als ein GCC-Fehler.

    Edit: Sieht aus als sei das eine MinGW-eigene Implementierung und zwar vermutlich diese hier (Version 5.4.1, die ich hier verwende):

    https://osdn.net/projects/mingw/scm/git/mingw-org-wsl/blobs/5.4-trunk/mingwrt/mingwex/math/pow_generic.sx

    Das ist eine etwas umfangreichere Assembler-Implementierung und mir defintitiv gerade zu fummelig, die zu debuggen. Mein Bauchgefühl sagt mir aber da ist irgendwo die Ursache zu finden.



  • @Swordfish sagte in Rechnung gibt falsches Ergebnis.:

    DAS macht mir Angst!?

    Wahrscheinlich wird hier im 32Bit Modus noch das 80Bit Float Format der Intel CPUs (das gab es sonst nur noch bei Motorola 68k und 88k) verwendet, und mit dem 64Bit Modus wird auf SSE/AVX gesetzt was nur IEEE Single und Double kennt.



  • @john-0 sagte in Rechnung gibt falsches Ergebnis.:

    @Swordfish sagte in Rechnung gibt falsches Ergebnis.:

    DAS macht mir Angst!?

    Wahrscheinlich wird hier im 32Bit Modus noch das 80Bit Float Format der Intel CPUs (das gab es sonst nur noch bei Motorola 68k und 88k) verwendet, und mit dem 64Bit Modus wird auf SSE/AVX gesetzt was nur IEEE Single und Double kennt.

    Selbst ein zwölffingriger Kobold im Gehäuse, der mit einem Duodezimalsystem-Rechenschieber arbeitet, muss stets zum selben Ergebnis kommen wenn er exakt die selbe Berechnung durchführt. Was immer hier sonst noch seltsam läuft, hier gibt es mindestens einen Bug.



  • @Finnegan sagte in Rechnung gibt falsches Ergebnis.:

    Selbst ein zwölffingriger Kobold im Gehäuse, der mit einem Duodezimalsystem-Rechenschieber arbeitet, muss stets zum selben Ergebnis kommen wenn er exakt die selbe Berechnung durchführt.

    Aber das ist nicht der Fall. Es wird im 80Bit Float Format anders gerundet als im IEEE Modus!



  • @Th69 sagte in Rechnung gibt falsches Ergebnis.:

    type operator -= (const type &a, int b)
    

    Ui. Das ist ein gutes Argument. Für einen überladenen Operator ist -= eine Funktion f(int) in diesem Fall. Aus der Perspektive wäre es natürlich auch konsistent, zuerst das Argument nach int zu konvertieren.

    Allerdings kann man auch einen int::operator-=(double) implementieren, der die Berechnung so durchführt, dass sie sich exakt wie x = x - y verhält. Das ist hier bei den eigebauten Typen vom Konzept her wohl Fall:

    https://en.cppreference.com/w/cpp/language/operator_assignment:

    Builtin compound assignment
    [...]
    The behavior of every builtin compound-assignment expression E1 op= E2 [...] is exactly the same as the behavior of the expression E1 = E1 op E2 [...]



  • @john-0 sagte in Rechnung gibt falsches Ergebnis.:

    Aber das ist nicht der Fall. Es wird im 80Bit Float Format anders gerundet als im IEEE Modus!

    Ja, das ist aber eine "andere Berechnung" ich rede hier von "exakt der selben Berechnung" in (486 - pow(3, 5)) == (486 - pow(3, 5)). Die rechte und die linke Seite von == werden bei deinem Argument entweder beide in 80-Bit oder beide in IEEE durchgeführt. Der Vergleich muss also immer true liefern. Tut er aber nicht bei MinGW.org.



  • @Finnegan sagte in Rechnung gibt falsches Ergebnis.:

    Ja, das ist aber eine "andere Berechnung" ich rede hier von "exakt der selben Berechnung" in (486 - pow(3, 5)) == (486 - pow(3, 5)). Die rechte und die linke Seite von == werden bei deinem Argument entweder beide in 80-Bit oder beide in IEEE durchgeführt. Der Vergleich muss also immer true liefern. Tut er aber nicht bei MinGW.org.

    Nein, so etwas kann man bei Floats leider nicht aussagen. Es gibt etliche Faktoren, die das Ergebnis einer Float Berechnung verändern können, und solange man nicht explizit das IEEE Normverhalten aktiviert hat, kann der Compiler einen ziemlichen Unfug treiben. D.h. exakt die gleichen Rechnungen müssen explizit nicht das Gleiche ergeben – sie können. Lies Dir dazu die Doku von Intel, die IEEE Norm und das Compilerhandbuch durch.



  • @john-0 sagte in Rechnung gibt falsches Ergebnis.:

    [...] D.h. exakt die gleichen Rechnungen müssen explizit nicht das Gleiche ergeben – sie können. Lies Dir dazu die Doku von Intel, die IEEE Norm und das Compilerhandbuch durch.

    Auf verschiedenen CPUs, Betriebssystemen oder auch mit anderen Compiler-Flags gebe ich dir da ohne Widerspruch recht. Diese Probleme sind mir bekannt, wodurch man selbst auf AMD- und Intel-x86 mit IEEE-Floats auf standardkonforme Weise leicht unterschiedliche Ergebnisse erhalten kann.

    Wir reden hier aber von zwei identischen Berechnungen auf der selben CPU, mit den selben Compiler-Flags auf dem selben OS im selben Programm, ja sogar im selben Ausdruck. Ich kenne die Normen nicht auswendig, aber ich habe starke Zweifel, dass sich speziell dieses Szenario mit den Freiheiten die diese einer Implementation lassen erklären lässt.

    Meines wissens sind Float-Berechnungen schon deterministisch, aber eben nicht sonderlich portabel.


  • Mod

    Wieso ist das so überraschend für dich? Angenommen das Szenario mit dem 80Bit/64Bit Rechenwerk. Dann ist eine ganz valide Interpretation des Codes folgende Arbeitsanweisung (die auch sehr wahrscheinlich bei O0 genommen wird):

    1. Berechne die linke Seite (80 Bit Präzision)
    2. Speichere das Ergebnis irgendwo (wird zu 64 Bit gerundet), denn wir brauchen Platz im Rechenwerk für die nächste Rechnung
    3. Berechne die rechte Seite (80 Bit Präzision)
    4. Oh, nun soll ich die beiden Werte vergleichen! Der 2. Wert steht ja noch im Rechenwerk (80 Bit), nun muss ich nur noch den 1. Wert wieder in das andere Register vom Rechenwerk laden.
    5. Jetzt wird der 80 Bit-Wert mit dem gerundeten 64-Bit-Wert verglichen.
    6. -> false




  • @SeppJ sagte in Rechnung gibt falsches Ergebnis.:

    Wieso ist das so überraschend für dich?

    Danke für die Aufklärung. Das ist in der Tat überraschend für mich, dass hier verglichen wird, ohne vorher sicherzustellen, dass der zweite Wert ebenfalls gerundet wird.

    Ich habe mich bisher tatsächlich darauf verlassen, dass ich das exakt selbe Ergebnis erhalte, wenn ich identische Rechenanweisungen in meinem Code durchführe. Egal wie die interne Repräsentation in den CPU-Registern aussieht und wie viele Rundungsfehler meine Berechnungen einbringen.

    Ich bin sogar entsetzt, wenn das wirklich alles so Standardkonform sein sollte. Das heisst wenn ich mich darauf verlassen muss, das zwei indentische FP-Berechnugen zum bitgenau selben Ergebnis kommen, verwende ich besser eine wohldefinierte FP-Emulation via Integer - auch wenn ich dasselbe Ergebnis im selben Programm mit exakt den selben Compiler-Flags und ordentlich initialisierter FPU benötige?

    Edit: Oder eben dem Compiler verklickern, solche Spässe zu unterlassen: -ffloat-store sorgt hier tatsächlich dafür, dass auch MinGW.org true ausgibt. Danke für den SO-Link!



  • @Finnegan sagte in Rechnung gibt falsches Ergebnis.:

    Meines wissens sind Float-Berechnungen schon deterministisch, aber eben nicht sonderlich portabel.

    Das ist nur dann garantiert, wenn man explizit das IEEE Normverhalten durch den Compiler aktiviert, und einen Rattenschwanz an Nebenbedingungen einhält. Das wird schon problematisch, wenn ein Autovektorisierer am Werk ist, und wird richtig problematisch, wenn Threading im Spiel ist. Denn die Reihenfolge von Additionen oder Multiplikationen spielt bei der Reproduzierbarkeit der Ergebnisse eine nicht unwesentliche Rolle. Da es so etwas wie out-of-order execution gibt, kann das auch eine Rolle spielen. Was genau der Compiler hier treibt ist unklar, die Masse wird hier true zurückliefern, wie man das erwartet.



  • @john-0 sagte in Rechnung gibt falsches Ergebnis.:

    @Finnegan sagte in Rechnung gibt falsches Ergebnis.:

    Meines wissens sind Float-Berechnungen schon deterministisch, aber eben nicht sonderlich portabel.

    Das ist nur dann garantiert, wenn man explizit das IEEE Normverhalten durch den Compiler aktiviert, [...]

    Jo danke, wieder was dazugelernt. Das mit der 80bit-Repräsentation und IEEE waren mir zwar bekannt, aber ich dachte bisher tatsächlich, dass sich das nur auswirkt, wenn man zwei mit unterschiedlichen Compiler-Flags gebaute Programme hat, oder sogar andere CPUs/Architekturen. Separate Threads hätte ich auch noch verstanden, wenn sie auf verschiedenen Kernen laufen. Aber im selben Ausdruck finde ich schon erschreckend 😉

    Nicht leicht, wenn man z.B. bei einer komplexeren physikalischen Simulation mit identischen Eingabewerten exakt den selben Endzustand reproduzieren will.



  • @Finnegan sagte in Rechnung gibt falsches Ergebnis.:

    Edit: Oder eben dem Compiler verklickern, solche Spässe zu unterlassen: -ffloat-store sorgt hier tatsächlich dafür, dass auch MinGW.org true ausgibt. Danke für den SO-Link!

    Exakt das 80Bit vs. 64Bit Problem und die Erklärung von SeppJ traf ins Schwarze.



  • @john-0 sagte in Rechnung gibt falsches Ergebnis.:

    Exakt das 80Bit vs. 64Bit Problem und die Erklärung von SeppJ traf ins Schwarze.

    Sieht so aus. Ich werd erstmal den Abend mit Kopfschütteln verbringen. Da bei mir grad ein Teil meines Weltbildes kaputtgegangen. Zum Glück habe ich noch keine Programme geschrieben, die wegen sowas spontan explodieren können 😉


  • Mod

    Sieh es mal so: Floating-Point ist quasi gemacht, um im Computer so etwas wie ein Zahlenkontinuum zu haben. Typische Nutzer dafür sind physikalische und ingenierustechnische Berechnungen, und ähnliches. Aber eben nicht Zahlentheoretiker, Bankkaufleute, etc. Die sind mit ganzen Zahlen schon perfekt bedient. Für die vorgesehenen Benutzer ist absolute Gleichheit praktisch nie entscheidend. Das sind alles stetige, hinreichend gutartige Funktionen. Es geht schlimmstenfalls um Differenzen, und die Größenordnung derselben. Und die Größenordnung der Differenz ist hier korrekt ziemlich nahe bei 0.



  • @SeppJ sagte in Rechnung gibt falsches Ergebnis.:

    [...] Für die vorgesehenen Benutzer ist absolute Gleichheit praktisch nie entscheidend. Das sind alles stetige, hinreichend gutartige Funktionen.

    Es geht mir auch weniger um Gleicheit, als um Reproduzierbarkeit. Ich hatte die Problematik ja schonmal angesprochen für ein Netzwerkspiel lediglich User-Eigaben übers Netzwerk zu übertragen und die Spielwelt in einer Lockstep-Simulation auf allen Clients synchron zu halten. Das spart eine Menge Traffic und skaliert gut. Das ist wohl eine ziemliche Herausforderung, sowas zuverlässig hinzubekommen, so dass auch die Performance stimmt.

    Es stimmt aber, dass das wohl ein etwas exotischeres Problem ist. Hier schlagen diese Rundungsgeschichten aber voll durch. Aber auch für Physiker und Ingenieure wäre es sicher nicht verkehrt, wenn länger laufende identische Simulationen im selben Endzustand landen.


  • Mod

    Für nicht-chaotische Systeme laufen sie ja auch in den (effektiv) gleichen Zustand, trotz minimaler Rundungsfehler, das ist ja gerade mein Punkt. Für chaotische Systeme natürlich nicht. Aber in dem Fall ist das schon fast ein Feature, weil es gewissermaßen der Realität entspricht 😃

    Das sollte für das Netzwerkspiel genauso gelten, das ist ja quasi eine Physiksimulation. Wenn da in der 14. Nachkommastelle was nicht exakt ist, dann macht das den millionsten Teil von einem Pixel. 64 Bit Präzision sind wahnsinnig exakt, wenn man mal darüber nachdenkt. Viel genauer als selbst die genauesten Messungen von egal was in der Realität. Daher reicht für so etwas wie Computergrafik auch 32 Bit in der Renderpipeline auch ganz locker aus. Da bekommst du viel eher und viel größere Fehler als durch deine durch double-Rundungsfehler verursachte Gamephysikungenauigkeit.



  • @SeppJ sagte in Rechnung gibt falsches Ergebnis.:

    Für nicht-chaotische Systeme laufen sie ja auch in den (effektiv) gleichen Zustand, trotz minimaler Rundungsfehler, das ist ja gerade mein Punkt. Für chaotische Systeme natürlich nicht. Aber in dem Fall ist das schon fast ein Feature, weil es gewissermaßen der Realität entspricht 😃

    Ja, und jeder trägt seine eigene Realität in seinem Kopf spazieren. Das ist zwar auch wie in der physischen Welt, für ein Spiel aber dennoch nicht optimal - besonders wenn man glaubwürdig darstellen will, wer den jetzt getroffen und wer daneben geschossen hat. Oder wenn man entscheiden will, welcher Cheater jetzt gekickt werden soll, weil er seine Simulation manipuliert hat (auch ein wichtiger Punkt), da sind bit-identsiche Spielzustände nützlich.

    Das sollte für das Netzwerkspiel genauso gelten, das ist ja quasi eine Physiksimulation. Wenn da in der 14. Nachkommastelle was nicht exakt ist, dann macht das den millionsten Teil von einem Pixel.

    Kommt auch drauf an wie gross die Welt ist. Für den "Screen Space" (nach Transformation der Weltobjekte in diesen) mag das zutreffen, aber die Simulationen in Weltkoordinaten der Spielwelt können sich schonmal über ein paar Kilometer erstrecken - je nachdem um was für ein Spiel es sich handelt.

    Für grosse Welten wird man dann aber wohl auch verschiedene hierarchische Koordinatensysteme haben. Denke nicht das so ein aktueller MS Flugsimulator für die gesamte Erde mit einem einzigen double-basierten Koordinatensystem arbeitet.

    Ich habe jedenfalls schon Rundungsfehler debuggt, wo das Objekt in der Welt mehrere Meter neben seiner korrekten Position gerendert wurde. Da meinte aber auch ein Entwickler, alle Transformationen fürs Rendering in Weltkoodinaten durchführen zu müssen - 10.000km vom Weltursprung entfernt 😉

    64 Bit Präzision sind wahnsinnig exakt, wenn man mal darüber nachdenkt. Viel genauer als selbst die genauesten Messungen von egal was in der Realität. Daher reicht für so etwas wie Computergrafik auch 32 Bit in der Renderpipeline auch ganz locker aus. Da bekommst du viel eher und viel größere Fehler als durch deine durch double-Rundungsfehler verursachte Gamephysikungenauigkeit.

    Ich bin mir gar nicht so sicher, ob diese Fehler dazu führen, dass das ganze System immer weiter divergiert, oder ob sich die Fehler über die Zeit alle ausgleichen. Denke auch diese 80/64-Bit-Geschichte lässt sich ebenfalls mit numerisch stabilen Algorithmen in den Griff bekommen, wie auch die durch sonstige FP-Arithmetik ohnehin schon verursachten Fehler - die muss man sowieso schon berücksichtigen.


Anmelden zum Antworten