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 volatile
Schlü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:fast
Option in Visual Studio 2008 aktiviere, ist das Ergebnis immer noch korrekt.
Weitere Frage:
Ich habe mich gefragt, ob ich die -ffloat-store
Option 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:fast
Visual Studio 2008 überhaupt noch funktioniert? Es scheint, dass Visual Studio 2008 bei diesem Problem zuverlässiger ist als g ++?
quelle
Antworten:
Intel x86-Prozessoren verwenden intern eine erweiterte 80-Bit-Genauigkeit, während sie
double
normalerweise 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-store
Option gcc, um dieselben Gleitkommaergebnisse mit unterschiedlichen Optimierungsstufen zu erhalten.Verwenden Sie alternativ den
long double
Typ, der bei gcc normalerweise 80 Bit breit ist, um eine Rundung von 80 Bit auf 64 Bit zu vermeiden.man gcc
das sagt alles:In x86_64-Builds verwenden Compiler SSE-Register für
float
unddouble
standardmäßig, sodass keine erweiterte Genauigkeit verwendet wird und dieses Problem nicht auftritt.gcc
Die Compiler-Option-mfpmath
steuert dies.quelle
inf
. Es gibt keine gute Faustregel, Unit-Tests können Ihnen eine eindeutige Antwort geben.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.
quelle
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:fast
gegen 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.
quelle
-ffast-math
Schalter für GCC, der von keiner der-O
Optimierungsstufen 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."-ffast-math
und ein paar andere Dinge auf meinemg++ 4.4.3
und ich bin immer noch nicht in der Lage, das Problem zu reproduzieren.-ffast-math
bekomme ich4.5
in beiden Fällen Optimierungsstufen größer als0
.4.5
mit-O1
und-O2
, aber nicht mit-O0
und-O3
in GCC 4.4.3, sondern mit-O1,2,3
in GCC 4.6.1.)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
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-store
dass 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()
(incmath
) undstd::numeric_limits<double>::epsilon()
(inlimits
) betrachten. Wennround()
ich über die ursprüngliche Funktion nachdenke, glaube ich, dass es sauberer wäre, den Aufruf vonstd::floor(d + .5)
durch einen Aufruf dieser Funktion zu ersetzen :Ich denke, das deutet auf folgende Verbesserung hin:
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 Summed
,.5
undstd::numeric_limits<double>::epsilon()
sollte in der Nähe von 1, so dass zusätzlich Mittel gruppieren , diestd::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()
.quelle
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ürepsilon()
enthält ein Beispiel für die Skalierung, um einen ULP-basierten relativen Fehler zu erhalten .-ffloat-store
lautet die Antwort für 2016 : Verwenden Sie x87 überhaupt nicht. Verwenden Sie SSE2-Mathematik (64-Bit-Binärdateien oder-mfpmath=sse -msse2
zum Erstellen knuspriger alter 32-Bit-Binärdateien), da SSE / SSE2 temporäre Elemente ohne zusätzliche Genauigkeit enthält.double
undfloat
vars 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.)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:
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 :-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 :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.
quelle
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):
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:
und unter Linux / Cygwin:
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=387
oder 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: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 machenlibdfp
!quelle
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.
quelle