Geanuigkeit abhängig von compiler?



  • Hallo ihr Lieben,

    mal wieder eine Frage (ist dies das richtige Unterforum?):

    Ich habe das Kuriosum, dass mein Programm auf zwei verschiedenen PCs verschieden läuft, nämlich derart, dass es einmal abstürzt und einmal durchläuft.

    Das mir die Interpolation der GSL abstürzt habe ich die leise Vermutung, dass die Genauigkeit nicht ausreicht? Also meine naive Frage: Kann die Genauigkeit (von double) vom PC abhängen?

    Falls ja, lässt sich das vereinheitlichen?

    Ich weiß, es ist nicht viel an Infos, aber das ein Programm je nach PC sich unterschiedlich verhält ist doch mal was neues. 😕

    Gruß,
    -- Klaus.



  • Sowas passiert mir leider öfters.
    Benutzt du threads?
    Hast du mal einen Debugger benutzt um zu testen wieso/wo das Programm abstürzt?



  • Erstmal solltest du die üblichen Verdächtigen abklappern, bevor du solche abseitigen Theorien verfolgst. Denn selbst wenn die Genauigkeit sich unterscheidet, wieso sollte das Programm deshalb abstürzen? Hast du das Programm eigentlich auf dem anderen Rechner neu compiliert oder ist das das gleiche Binary?



  • nwp3 schrieb:

    Sowas passiert mir leider öfters.

    Tatsache?

    nwp3 schrieb:

    Benutzt du threads?

    Nein.

    nwp3 schrieb:

    Hast du mal einen Debugger benutzt um zu testen wieso/wo das Programm abstürzt?

    Schon, um zu schauen wo es abstürzt. Kann das leider jetzt nicht reproduzieren bzw. posten, denn "hier" funktioniert es ja. 😉

    Bashar schrieb:

    Erstmal solltest du die üblichen Verdächtigen abklappern, bevor du solche abseitigen Theorien verfolgst. Denn selbst wenn die Genauigkeit sich unterscheidet, wieso sollte das Programm deshalb abstürzen? Hast du das Programm eigentlich auf dem anderen Rechner neu compiliert oder ist das das gleiche Binary?

    Neu kompiliert.

    Gruß,
    -- Klaus.


  • Mod

    Erstell mal ein Minimalbeispiel. Eines, das bei dir auf einem Rechner funktioniert, auf einem anderen Rechner nicht. Auch wenn das Programm dann hier bei uns funktionieren mag, erkennen wir vielleicht trotzdem mögliche Ursachen im Quelltext.

    Deine Beschreibung klingt jedenfalls schwer danach, als würdest du undefiniertes Verhalten erzeugen. Das kann allerlei Ursachen haben. Da die GSL eine C-Bibliothek ohne native C++-Bindings ist, rate ich einfach mal ins Blaue, dass du deine Wrapper falsch implementiert hast (oder du gar keine Wrapper hast und die GSL schlichtweg falsch benutzt).



  • Die Rechengenauigkeit ist mit Sicherheit NICHT abhaengig vom Compiler. Alle mir bekannten Computer halten sich an den IEEE 754 Standard, der sowas definiert.

    Was allerdings vom Compiler abhaengen koennte, ist die Repraesentation von Fliesskomma-Konstanten in deinem Source.



  • Okay,

    mehr dann heute Abend, wenn ich den Fehler reproduzieren kann. 🙂

    Gruß,
    -- Klaus.



  • Die meisten populären Maschinen werden wohl IEEE-754 mit 32Bit floats und 64-Bit doubles unterstützen, wobei die Intelmaschinen intern sogar mit bis zu 80 Bit rechnen und beim Speichern im RAM daraus wieder 64 Bit machen. Der C++ Standard legt aber nur fest, dass ein float eine Genauigkeit von mindestens 6 Dezimalstellen und double und long double mindestens 10 Dezimalstellen besitzen müssen. Damit sind aber nicht Nachkommastellen gemeint. Die Vorkommastellen zählen da auch. Der Standard garantiert mir also, dass es zum Beispiel mindestens ein float-Bitmuster gibt, was die Zahl 123.456+f repräsentiert, wobei |f|<0.0005 gilt.



  • Kellerautomat schrieb:

    Die Rechengenauigkeit ist mit Sicherheit NICHT abhaengig vom Compiler. Alle mir bekannten Computer halten sich an den IEEE 754 Standard, der sowas definiert.

    ...zumindest solange man die Finger von -ffast-math und ähnlichen Compileroptionen lässt. Da du GSL erwähnt hast, solltest du auch checken mit welchen Flags deine Bibliotheken compiliert wurden, falls du hier fertige Binaries verwendest.



  • Das Ergebnis von Fließkommarechnung im unteren Teil der Mantisse kann sich durchaus von Compiler zu Compiler mit dem selben Code und den selben Eingabedaten unterscheiden. Und gerade bei mäßig stabilen Suchverfahren kann sich so was auch mal zu spürbar anderen Ergebnissen führen. Ich seh das jetzt nicht mit großer Regelmäßigkeit, aber so was kommt schon mal vor. Meistens handelt es sich dabei um Unterschiede zwischen 32- und 64-Bit-Kompilaten, mitunter liefert aber auch so mit MSVC kompilierter Code andere Ergebnisse als gcc für die selbe Architektur.

    -ffast-math und dergleichen ist ein offensichtlicher Kandidat, aber mitnichten der einzige -- IEEE-754 definiert das alles nicht so genau, wie Kellerautomat und Desdemona zu glauben scheinen; zudem ist es leicht, in Bereiche vorzustoßen, die IEEE-754 gar nicht abdeckt. Ob ein Float durch den x87 oder durch SSE gelaufen ist, kann schon einen Unterschied machen, und wenn ein optimierender Compiler Ausdrücke umstellt, können sich Rundungsfehler verschieben. Das kann sogar durch constant-folding auftreten.

    Jetzt bewegen sich solche Fehler in der einzelnen Operation im untersten Teil der Mantisse, aber bei bestimmten komplexen Berechnungen kann sich so was aufschaukeln. Das Davidon-Fletcher-Powell-Suchverfahren zum Beispiel ist für solche Scherze gern mal zu haben (Drecksding).

    Einen Absturz erklärt das allerdings nicht, sofern die GSL nicht in der FPE rumspielt. Was durchaus sein kann. Stürzt das Ding mit nem SIGFPE ab?



  • @seldon
    Schreibt IEEE 754 nicht vor dass die 4 Grundrechenarten auf 1/1 ULP genau runden müssen?
    Und bedeutet das nicht dass die Ergebnisse immer 100% identisch sein müssen?



  • hustbaer schrieb:

    Schreibt IEEE 754 nicht vor dass die 4 Grundrechenarten auf 1/1 ULP genau runden müssen?
    Und bedeutet das nicht dass die Ergebnisse immer 100% identisch sein müssen?

    Der C++ Standard schreibt aber nicht vor, dass Fließkommazahlen dem IEEE 754 Standard folgen müssen... 😉

    Aber gut, heutzutage tun sie das wohl auf jeder vernünftigen Plattform. Selbst dann ist es aber so, dass es je nach Compiler und verwendeten Flags Unterschiede geben kann und auch tatsächlich tut. Auf x86 haben wir z.B. auf der einen Seite die alte x87 FPU, die intern mit höherer Genauigkeit rechnet und auf der anderen Seite SSE, AVX etc. Auf welche Instruktionen unser Code abgebildet wird, ist schonmal völlig Compilerabhängig. Abgesehen davon, kann man den gängigen Compilern per Flag erlauben, zugunsten gewisser Optimierungen den IEEE Standard nichtmehr ganz so genau zu nehmen...



  • Für die Grundrechenarten (und die Quadratwurzel) schon, abhängig vom Rundungsmodus, aber darauf wird sich die GSL sicher nicht beschränken. Für transzendentale Funktionen (sin, exp, log etc.) ist das Table-Maker's Dilemma nach wie vor nicht vollständig gelöst, und ieee-754 verlangt daher auch nichts entsprechendes für sie.

    Zudem bedeutet das auch dann noch nicht, dass die Ergebnisse 100% übereinstimmen werden, wenn man sich auf Grundrechenarten beschränkt. So rechnet der x87-Coprozessor intern mit 80 Bit breiten Zwischenergebnissen, während die SSE das nicht tun. Beide sind ieee-754-konform, in beiden Fällen wird für einen Ausdruck x + y das gleiche Ergebnis herauskommen, aber ein Ausdruck x + y + y kann, wenn y passend kleiner als x gewählt ist, so dass x + y == x, aber x + 2 * y != x ist, entweder x sein oder 1 ulp von x entfernt liegen. Mit dieser Problematik befasst ieee-754 sich nicht, und die C- und C++-Standards, welche IEEE-754-Konformität ohnehin nicht verlangen, schweigen sich darüber aus, ob und wann Zwischenergebnisse abgeschnitten werden sollten. In der Praxis kann das jedenfalls mal so, mal so laufen.



  • Ich habe es gerade mal ausprobiert (Win7 64 bit), intel CPU:

    #include <iostream>
    #include <cstdlib>
    #include <cstring>
    
    int main()
    {
    	double a = 0.3123, b = 0.1234, c = 2.32345;
    	for (unsigned i = 0; i != 10; ++i)
    	{
    		a = a * b * c;
    		a += 64.234;
    		a /= 10.3;
    		b += 1.324;
    	}
    	std::size_t A;
    	std::memcpy(&A, &a, 8); //assume sizeof(double) = sizeof(size_t) = 8
    	std::cout << std::hex << A;
    }
    

    g++ 4.7.1 (hab grad keine neuere Version) mit -O3 Output:

    4094e959c8a6245a
    

    VS13 mit vollen Optimierungen:

    4094e959c8a6245c
    

    Ich habe extra nicht long double verwendet, weil gcc damit auf den 80-bit Registern rechnet, während VS immer die 64 bit xmm Register benutzt.

    Wobei das hier nicht ganz fair ist, weil der gcc das alles in eine Konstante kompiliert...
    gcc:

    movabsq	$4653600886808257626, %rdx
    

    Der VS scheint das zu berechnen (/Ox /Ot /fp:fast) [was mich ein wenig wundert]

    movsdx	xmm1, QWORD PTR __real@3fbf972474538ef3
    	movsdx	xmm2, QWORD PTR __real@4002966cf41f212d
    	movsdx	xmm3, QWORD PTR __real@40500ef9db22d0e5
    	movaps	XMMWORD PTR [rsp+32], xmm6
    	mov	eax, 10
    	movsdx	xmm6, QWORD PTR __real@3fd3fcb923a29c78
    	movsdx	xmm4, QWORD PTR __real@3fb8dab7ec1dd343
    	movsdx	xmm5, QWORD PTR __real@3ff52f1a9fbe76c9
    $LL16@main:
    	addsd	xmm1, xmm5
    	mulsd	xmm0, xmm6
    	movaps	xmm6, xmm0
    	mulsd	xmm6, xmm2
    	addsd	xmm6, xmm3
    	mulsd	xmm6, xmm4
    	dec	rax
    jne	SHORT $LL16@main
    

    Ein weiterer vom Compiler abhängiger Faktor ist noch, ob in der CPU "flush to zero" gesetzt ist, also denormalisierte Zahlen zu null gesetzt werden.



  • Ohne fp:fast (also mit default-Einstellung, fp:precise) kommt halt genau so 4094e959c8a6245a raus..



  • Hier stand Blödsinn,

    ich bin einfach noch zu doof für die move Semantik. 😡

    Gruß,
    -- Klaus.



  • Wenn ich den Code leicht abwandle, so dass der gcc die Konstanten nicht zusammenfalten kann und das ganze 32-bit-kompatibel mache:

    #include <iostream>
    #include <cstdlib>
    #include <cstring>
    #include <cstdint>
    
    int main()
    {
        double a = 0.3123, b = 0.1234, c = 2.32345;
        unsigned max;
        std::cin >> max;
    
        for (unsigned i = 0; i != max; ++i)
        {
            a = a * b * c;
            a += 64.234;
            a /= 10.3;
            b += 1.324;
        }
        std::uint64_t A;
        std::memcpy(&A, &a, 8); //assume sizeof(double) = sizeof(size_t) = 8
        std::cout << std::hex << A << '\n';
    }
    

    Dann kriege ich

    $ for opt in "-O2" "-m32 -O2" ; do g++ -std=c++11 $opt foo.cc && echo 10 | ./a.out; done
    4094e959c8a6245a
    4094e959c8a6245f
    


  • Klaus82 schrieb:

    Hier stand Blödsinn,

    ich bin einfach noch zu doof für die move Semantik. 😡

    Soll das heißen, dass Du einen Bug in Deinem Code gefunden hast und das Thema damit erledigt ist?



  • Furble Wurble schrieb:

    Klaus82 schrieb:

    Hier stand Blödsinn,

    ich bin einfach noch zu doof für die move Semantik. 😡

    Soll das heißen, dass Du einen Bug in Deinem Code gefunden hast und das Thema damit erledigt ist?

    Ich denke schon. Zumindest läuft er jetzt sehr stabil und stürzt nicht ab, wenn ich Variablen variiere, die vorher zum Crash geführt haben.

    Gruß,
    -- Klaus.



  • Mein Beitrag war auf

    seldon schrieb:

    IEEE-754 definiert das alles nicht so genau, wie Kellerautomat und Desdemona zu glauben scheinen

    bezogen.
    Die transzendenten Funktionen sind aber natürlich ein Problem.

    Was die 80 Bit Register angeht... ich meine MSVC ist was das angeht ziemlich "paranoid", und kopiert in der Standardeinstellung alles dauernd um, um das "Abschneiden" auf 64 bzw. 32 Bit zu erzwingen.
    Die Ergebnisse sollten also mit jedem Compiler der ebenso paranoid ist reproduzierbar sein.

    Ausgenommen natürlich bei Funktionen wo IEEE-754 nicht die 1/2 ULP Genauigkeit fordert, bzw. bei Hardware die gleich gar nicht IEEE-754 konform ist (z.B. Pentium P5, hihi).


Log in to reply