Unterschiedliches Gleitkomma-Ergebnis bei aktivierter Optimierung - Compiler-Fehler?


109

Der folgende Code funktioniert in Visual Studio 2008 mit und ohne Optimierung. Es funktioniert aber nur unter g ++ ohne Optimierung (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Die Ausgabe sollte sein:

4.5
4.6

Aber g ++ mit Optimierung ( O1- O3) gibt aus:

4.5
4.5

Wenn ich das volatileSchlüsselwort vor t hinzufüge , funktioniert es. Könnte es also einen Optimierungsfehler geben?

Test auf g ++ 4.1.2 und 4.4.4.

Hier ist das Ergebnis auf ideone: http://ideone.com/Rz937

Und die Option, die ich auf g ++ teste, ist einfach:

g++ -O2 round.cpp

Das interessantere Ergebnis, auch wenn ich die /fp:fastOption in Visual Studio 2008 aktiviere, ist das Ergebnis immer noch korrekt.

Weitere Frage:

Ich habe mich gefragt, ob ich die -ffloat-storeOption immer aktivieren soll .

Da die von mir getestete g ++ - Version mit CentOS / Red Hat Linux 5 und CentOS / Redhat 6 geliefert wird .

Ich habe viele meiner Programme unter diesen Plattformen kompiliert und befürchte, dass dies zu unerwarteten Fehlern in meinen Programmen führen wird. Es scheint ein wenig schwierig zu sein, meinen gesamten C ++ - Code und die verwendeten Bibliotheken zu untersuchen, ob sie solche Probleme haben. Irgendein Vorschlag?

Interessiert sich jemand dafür, warum /fp:fastVisual Studio 2008 überhaupt noch funktioniert? Es scheint, dass Visual Studio 2008 bei diesem Problem zuverlässiger ist als g ++?


51
An alle neuen SO-Benutzer: So stellen Sie eine Frage. +1
vierundvierzig

1
FWIW, ich bekomme die richtige Ausgabe mit g ++ 4.5.0 mit MinGW.
Steve Blackwell

2
ideone verwendet 4.3.4 ideone.com/b8VXg
Daniel A. White

5
Sie sollten sich vor Augen halten, dass es unwahrscheinlich ist, dass Ihre Routine mit allen Arten von Ausgaben zuverlässig funktioniert. Im Gegensatz zum Runden eines Doppels auf eine Ganzzahl ist dies anfällig für die Tatsache, dass nicht alle reellen Zahlen dargestellt werden können. Sie sollten also damit rechnen, dass mehr Fehler wie dieser auftreten.
Jakub Wieczorek

2
Für diejenigen, die den Fehler nicht reproduzieren können: Kommentieren Sie die auskommentierten Debug-Stmts nicht aus, sie wirken sich auf das Ergebnis aus.
n. 'Pronomen' m.

Antworten:


91

Intel x86-Prozessoren verwenden intern eine erweiterte 80-Bit-Genauigkeit, während sie doublenormalerweise 64 Bit breit sind. Unterschiedliche Optimierungsstufen beeinflussen, wie oft Gleitkommawerte von der CPU im Speicher gespeichert und somit von 80-Bit-Genauigkeit auf 64-Bit-Genauigkeit gerundet werden.

Verwenden Sie die -ffloat-storeOption gcc, um dieselben Gleitkommaergebnisse mit unterschiedlichen Optimierungsstufen zu erhalten.

Verwenden Sie alternativ den long doubleTyp, der bei gcc normalerweise 80 Bit breit ist, um eine Rundung von 80 Bit auf 64 Bit zu vermeiden.

man gcc das sagt alles:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

In x86_64-Builds verwenden Compiler SSE-Register für floatund doublestandardmäßig, sodass keine erweiterte Genauigkeit verwendet wird und dieses Problem nicht auftritt.

gccDie Compiler-Option-mfpmath steuert dies.


20
Ich denke das ist die Antwort. Die Konstante 4,55 wird in 4,54999999999999 konvertiert. Dies ist die nächste binäre Darstellung in 64 Bit. Multiplizieren Sie mit 10 und runden Sie erneut auf 64 Bit. Sie erhalten 45,5. Wenn Sie den Rundungsschritt überspringen, indem Sie ihn in einem 80-Bit-Register aufbewahren, erhalten Sie 45.4999999999999.
Mark Ransom

Danke, ich kenne diese Option nicht einmal. Aber ich habe mich gefragt, ob ich immer die Option -ffloat-store aktivieren soll. Da die von mir getestete g ++ - Version mit CentOS / Redhat 5 und CentOS / Redhat 6 ausgeliefert wird. Ich habe viele meiner Programme unter diesen Plattformen kompiliert. Ich mache mir Sorgen, dass dies zu unerwarteten Fehlern in meinen Programmen führen kann.
Bär

5
@Bear, die Debug-Anweisung bewirkt wahrscheinlich, dass der Wert aus einem Register in den Speicher gelöscht wird.
Mark Ransom

2
@Bear, normalerweise sollte Ihre Anwendung von einer erweiterten Genauigkeit profitieren, es sei denn, sie arbeitet mit extrem kleinen oder großen Werten, wenn erwartet wird, dass ein 64-Bit-Float unter- oder überläuft und produziert inf. Es gibt keine gute Faustregel, Unit-Tests können Ihnen eine eindeutige Antwort geben.
Maxim Egorushkin

2
@bear Wenn Sie Ergebnisse benötigen, die perfekt vorhersehbar sind und / oder genau das, was ein Mensch auf dem Papier tun würde, sollten Sie Gleitkomma vermeiden. -ffloat-store beseitigt eine Quelle der Unvorhersehbarkeit, aber es ist kein Wundermittel.
Plugwash

10

Die Ausgabe sollte wie folgt lauten: 4.5 4.6 Dies wäre die Ausgabe, wenn Sie eine unendliche Genauigkeit hätten oder mit einem Gerät arbeiten würden, das eine dezimalbasierte statt eine binärbasierte Gleitkommadarstellung verwendet. Aber du bist es nicht. Die meisten Computer verwenden den binären IEEE-Gleitkomma-Standard.

Wie Maxim Yegorushkin bereits in seiner Antwort feststellte, besteht ein Teil des Problems darin, dass Ihr Computer intern eine 80-Bit-Gleitkommadarstellung verwendet. Dies ist jedoch nur ein Teil des Problems. Die Basis des Problems ist, dass eine beliebige Zahl der Form n.nn5 keine exakte binäre schwebende Darstellung hat. Diese Eckfälle sind immer ungenaue Zahlen.

Wenn Sie wirklich möchten, dass Ihre Rundung diese Eckfälle zuverlässig abrunden kann, benötigen Sie einen Rundungsalgorithmus, der die Tatsache berücksichtigt, dass n.n5, n.nn5 oder n.nnn5 usw. (aber nicht n.5) immer ist ungenau. Suchen Sie den Eckfall, der bestimmt, ob ein Eingabewert auf- oder abgerundet wird, und geben Sie den aufgerundeten oder abgerundeten Wert basierend auf einem Vergleich mit diesem Eckfall zurück. Und Sie müssen darauf achten, dass ein optimierender Compiler den gefundenen Eckfall nicht in ein erweitertes Präzisionsregister einfügt.

Siehe Wie kann Excel schwebende Zahlen erfolgreich umrunden, obwohl sie ungenau sind? für einen solchen Algorithmus.

Oder Sie können einfach damit leben, dass die Eckfälle manchmal fälschlicherweise rund werden.


6

Unterschiedliche Compiler haben unterschiedliche Optimierungseinstellungen. Einige dieser schnelleren Optimierungseinstellungen halten keine strengen Gleitkommaregeln gemäß IEEE 754 ein . Visual Studio hat eine bestimmte Einstellung, /fp:strict, /fp:precise, /fp:fast, wo /fp:fastgegen den Standard auf das, was getan werden kann. Möglicherweise steuert dieses Flag die Optimierung in solchen Einstellungen. Möglicherweise finden Sie auch eine ähnliche Einstellung in GCC, die das Verhalten ändert.

Wenn dies der Fall ist, unterscheidet sich die Compiler nur dadurch, dass GCC bei höheren Optimierungen standardmäßig nach dem schnellsten Gleitkomma-Verhalten sucht, während Visual Studio das Gleitkomma-Verhalten bei höheren Optimierungsstufen nicht ändert. Daher muss es sich nicht unbedingt um einen tatsächlichen Fehler handeln, sondern um das beabsichtigte Verhalten einer Option, von der Sie nicht wussten, dass Sie sie aktivieren.


4
Es gibt einen -ffast-mathSchalter für GCC, der von keiner der -OOptimierungsstufen seit dem Zitat aktiviert wird: "Es kann zu einer falschen Ausgabe für Programme führen, die von einer genauen Implementierung der IEEE- oder ISO-Regeln / Spezifikationen für mathematische Funktionen abhängen."
Mat

@Mat: Ich habe versucht -ffast-mathund ein paar andere Dinge auf meinem g++ 4.4.3und ich bin immer noch nicht in der Lage, das Problem zu reproduzieren.
NPE

Schön: mit -ffast-mathbekomme ich 4.5in beiden Fällen Optimierungsstufen größer als 0.
Kerrek SB

(Korrektur: Ich bekomme 4.5mit -O1und -O2, aber nicht mit -O0und -O3in GCC 4.4.3, sondern mit -O1,2,3in GCC 4.6.1.)
Kerrek SB

4

Für diejenigen, die den Fehler nicht reproduzieren können: Kommentieren Sie die auskommentierten Debug-Stmts nicht aus, sie wirken sich auf das Ergebnis aus.

Dies impliziert, dass das Problem mit den Debug-Anweisungen zusammenhängt. Und es sieht so aus, als ob es einen Rundungsfehler gibt, der durch das Laden der Werte in Register während der Ausgabeanweisungen verursacht wird, weshalb andere festgestellt haben, dass Sie dies beheben können-ffloat-store

Weitere Frage:

Ich habe mich gefragt, ob ich die -ffloat-storeOption immer aktivieren soll .

Um flippig zu sein, muss es einen Grund geben, warum sich einige Programmierer nicht einschalten -ffloat-store, sonst würde die Option nicht existieren (ebenso muss es einen Grund geben, warum einige Programmierer nicht einschalten nicht einschalten -ffloat-store). Ich würde nicht empfehlen, es immer ein- oder auszuschalten. Durch das Aktivieren werden einige Optimierungen verhindert, durch das Deaktivieren wird jedoch das Verhalten berücksichtigt, das Sie erhalten.

Im Allgemeinen gibt es jedoch eine gewisse Nichtübereinstimmung zwischen binären Gleitkommazahlen (wie vom Computer verwendet) und dezimalen Gleitkommazahlen (mit denen die Leute vertraut sind), und diese Nichtübereinstimmung kann ein ähnliches Verhalten verursachen wie das, was Sie erhalten (um klar zu sein, das Verhalten) Sie erhalten wird nicht durch diese Nichtübereinstimmung verursacht, aber ähnliches Verhalten kann sein). Die Sache ist, da Sie bereits einige Unbestimmtheiten im Umgang mit Gleitkomma haben, kann ich nicht sagen, -ffloat-storedass es besser oder schlechter wird.

Stattdessen möchten Sie vielleicht nach anderen Lösungen für das Problem suchen , das Sie lösen möchten (leider zeigt Koenig nicht auf das eigentliche Papier, und ich kann keinen offensichtlichen "kanonischen" Ort dafür finden, also ich Ich muss Sie an Google senden .


Wenn Sie nicht für Ausgabezwecke runden, würde ich wahrscheinlich std::modf()(in cmath) und std::numeric_limits<double>::epsilon()(in limits) betrachten. Wenn round()ich über die ursprüngliche Funktion nachdenke, glaube ich, dass es sauberer wäre, den Aufruf von std::floor(d + .5)durch einen Aufruf dieser Funktion zu ersetzen :

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

Ich denke, das deutet auf folgende Verbesserung hin:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Ein einfacher Hinweis: std::numeric_limits<T>::epsilon()ist definiert als "die kleinste Zahl, die zu 1 hinzugefügt wird und eine Zahl ungleich 1 erzeugt." Normalerweise müssen Sie ein relatives Epsilon verwenden (dh Epsilon irgendwie skalieren, um die Tatsache zu berücksichtigen, dass Sie mit anderen Zahlen als "1" arbeiten). Die Summe d, .5und std::numeric_limits<double>::epsilon()sollte in der Nähe von 1, so dass zusätzlich Mittel gruppieren , die std::numeric_limits<double>::epsilon()über die richtige Größe für das, was wir tun. Wenn überhaupt,std::numeric_limits<double>::epsilon() wird es zu groß sein (wenn die Summe aller drei kleiner als eins ist) und kann dazu führen, dass wir einige Zahlen aufrunden, wenn wir es nicht sollten.


Heutzutage sollten Sie überlegen std::nearbyint().


Ein "relatives Epsilon" heißt 1 ulp (1 Einheit an letzter Stelle). x - nextafter(x, INFINITY)ist verwandt mit 1 ulp für x (aber benutze das nicht; ich bin sicher, dass es Eckfälle gibt und ich habe das gerade erfunden). Das cppreference-Beispiel für epsilon() enthält ein Beispiel für die Skalierung, um einen ULP-basierten relativen Fehler zu erhalten .
Peter Cordes

2
Übrigens -ffloat-storelautet die Antwort für 2016 : Verwenden Sie x87 überhaupt nicht. Verwenden Sie SSE2-Mathematik (64-Bit-Binärdateien oder -mfpmath=sse -msse2zum Erstellen knuspriger alter 32-Bit-Binärdateien), da SSE / SSE2 temporäre Elemente ohne zusätzliche Genauigkeit enthält. doubleund floatvars in XMM-Registern sind wirklich im IEEE 64-Bit- oder 32-Bit-Format. (Im Gegensatz zu x87, wo die Register immer 80-Bit sind und im Speicher auf 32 oder 64 Bit gerundet werden.)
Peter Cordes

3

Die akzeptierte Antwort ist korrekt, wenn Sie auf ein x86-Ziel kompilieren, das SSE2 nicht enthält. Alle modernen x86-Prozessoren unterstützen SSE2. Wenn Sie dies nutzen können, sollten Sie:

-mfpmath=sse -msse2 -ffp-contract=off

Lassen Sie uns das zusammenfassen.

-mfpmath=sse -msse2. Dies führt eine Rundung durch Verwendung von SSE2-Registern durch, was viel schneller ist als das Speichern jedes Zwischenergebnisses im Speicher. Beachten Sie, dass dies in GCC für x86-64 bereits die Standardeinstellung ist . Aus dem GCC-Wiki :

Auf moderneren x86-Prozessoren, die SSE2 unterstützen, wird durch die Angabe der Compileroptionen -mfpmath=sse -msse2sichergestellt, dass alle Float- und Double-Operationen in SSE-Registern ausgeführt und korrekt gerundet werden. Diese Optionen wirken sich nicht auf den ABI aus und sollten daher nach Möglichkeit für vorhersagbare numerische Ergebnisse verwendet werden.

-ffp-contract=off. Die Kontrolle der Rundung reicht jedoch nicht für eine genaue Übereinstimmung aus. FMA-Anweisungen (Fused Multiply-Add) können das Rundungsverhalten im Vergleich zu nicht fusionierten Gegenstücken ändern. Daher müssen wir es deaktivieren. Dies ist die Standardeinstellung für Clang, nicht für GCC. Wie durch diese Antwort erklärt :

Eine FMA hat nur eine Rundung (sie behält effektiv die unendliche Genauigkeit für das interne temporäre Multiplikationsergebnis bei), während eine ADD + MUL zwei hat.

Durch Deaktivieren von FMA erhalten wir Ergebnisse, die beim Debuggen und Freigeben genau übereinstimmen, auf Kosten einer gewissen Leistung (und Genauigkeit). Wir können weiterhin andere Leistungsvorteile von SSE und AVX nutzen.


1

Ich habe mich mehr mit diesem Problem befasst und kann mehr Präzisionen bringen. Erstens sind die genauen Darstellungen von 4.45 und 4.55 gemäß gcc auf x84_64 die folgenden (mit libquadmath, um die letzte Genauigkeit zu drucken):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Wie Maxim oben sagte, ist das Problem auf die 80-Bit-Größe der FPU-Register zurückzuführen.

Aber warum tritt das Problem unter Windows nie auf? Auf IA-32 wurde die x87-FPU so konfiguriert, dass eine interne Genauigkeit für die Mantisse von 53 Bit verwendet wird (entspricht einer Gesamtgröße von 64 Bit :) double. Für Linux und Mac OS wurde die Standardgenauigkeit von 64 Bit verwendet (entspricht einer Gesamtgröße von 80 Bit :) long double. Das Problem sollte also auf diesen verschiedenen Plattformen möglich sein oder nicht, indem das Steuerwort der FPU geändert wird (vorausgesetzt, die Reihenfolge der Anweisungen würde den Fehler auslösen). Das Problem wurde gcc als Fehler 323 gemeldet (lesen Sie mindestens den Kommentar 92!).

Um die Mantissengenauigkeit unter Windows zu zeigen, können Sie diese mit VC ++ in 32 Bit kompilieren:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

und unter Linux / Cygwin:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Beachten Sie, dass Sie mit gcc die FPU-Genauigkeit mit einstellen können -mpc32/64/80 , obwohl sie in Cygwin ignoriert wird. Denken Sie jedoch daran, dass dadurch die Größe der Mantisse geändert wird, nicht jedoch die des Exponenten, wodurch die Tür für andere Verhaltensweisen geöffnet wird.

In der x86_64-Architektur wird SSE wie von tmandry angegeben verwendet , sodass das Problem nur auftritt, wenn Sie die alte x87-FPU für FP-Computing erzwingen -mfpmath=387oder wenn Sie im 32-Bit-Modus mit kompilieren -m32(Sie benötigen ein Multilib-Paket). Ich könnte das Problem unter Linux mit verschiedenen Kombinationen von Flags und Versionen von gcc reproduzieren:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

Ich habe einige Kombinationen unter Windows oder Cygwin mit VC ++ / gcc / tcc ausprobiert, aber der Fehler ist nie aufgetreten. Ich nehme an, die Reihenfolge der generierten Anweisungen ist nicht dieselbe.

Beachten Sie schließlich, dass ein exotischer Weg, um dieses Problem mit 4.45 oder 4.55 zu verhindern, darin besteht, es zu verwenden _Decimal32/64/128, aber der Support ist wirklich knapp ... Ich habe viel Zeit damit verbracht, nur einen Ausdruck mit zu machen libdfp!


0

Persönlich habe ich das gleiche Problem in die andere Richtung - von gcc bis VS. In den meisten Fällen halte ich es für besser, eine Optimierung zu vermeiden. Es lohnt sich nur, wenn Sie sich mit numerischen Methoden befassen, die große Arrays von Gleitkommadaten umfassen. Selbst nach dem Zerlegen bin ich oft von den Entscheidungen des Compilers überwältigt. Sehr oft ist es einfacher, Compiler-Intrinsics zu verwenden oder die Assembly einfach selbst zu schreiben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.