Bei meiner Frage geht es nicht um schwebende Präzision. Es geht darum, warum Equals()
anders ist als ==
.
Ich verstehe warum .1f + .2f == .3f
ist false
(während .1m + .2m == .3m
ist true
).
Ich verstehe, das ==
ist Referenz und .Equals()
ist Wertvergleich. ( Bearbeiten : Ich weiß, dass mehr dahinter steckt .)
Aber warum ist (.1f + .2f).Equals(.3f)
true
, solange (.1d+.2d).Equals(.3d)
es noch ist false
?
.1f + .2f == .3f; // false
(.1f + .2f).Equals(.3f); // true
(.1d + .2d).Equals(.3d); // false
Math.Abs(.1d + .2d - .3d) < double.Epsilon
Dies sollte die bessere Gleichstellungsmethode sein.==
ist nicht „Referenz“ Vergleich und.Equals()
ist nicht „Wert“ Vergleich. Ihre Implementierung ist typspezifisch.0.1 + 0.2 == 0.3
um einen konstanten Ausdruck handelt , der zur Kompilierungszeit vollständig berechnet werden kann. In(0.1 + 0.2).Equals(0.3)
der0.1 + 0.2
und die0.3
sind alle konstante Ausdrücke aber die Gleichheit wird durch die Laufzeit berechnet wird , nicht durch den Compiler. Ist das klar?Antworten:
Die Frage ist verwirrend formuliert. Lassen Sie es uns in viele kleinere Fragen aufteilen:
Lassen Sie mich Ihnen eine Analogie geben. Angenommen, wir haben ein mathematisches System, bei dem alle Zahlen auf genau fünf Dezimalstellen gerundet sind. Angenommen, Sie sagen:
x = 1.00000 / 3.00000;
Sie würden erwarten, dass x 0,33333 ist, richtig? Denn das ist die Zahl in unserem System, die der tatsächlichen Antwort am nächsten kommt . Angenommen, Sie haben gesagt
y = 2.00000 / 3.00000;
Sie würden erwarten, dass y 0,66667 ist, richtig? Denn auch dies ist die Zahl in unserem System, die der tatsächlichen Antwort am nächsten kommt . 0,66666 ist weiter von zwei Dritteln als 0,66667 ist.
Beachten Sie, dass wir im ersten Fall abgerundet und im zweiten Fall aufgerundet haben.
Nun, wenn wir sagen
was bekommen wir Wenn wir genau rechnen würden, wäre jede davon offensichtlich vier Drittel und sie wären alle gleich. Aber sie sind nicht gleich. Obwohl 1,33333 in unserem System vier Dritteln am nächsten kommt, hat nur r diesen Wert.
q ist 1.33332 - da x ein bisschen klein war, hat jede Addition diesen Fehler akkumuliert und das Endergebnis ist ein bisschen zu klein. Ebenso ist s zu groß; es ist 1.33334, weil y ein bisschen zu groß war. r erhält die richtige Antwort, weil die zu große Größe von y durch die zu kleine Größe von x aufgehoben wird und das Ergebnis korrekt ist.
Ja; Eine höhere Genauigkeit verringert die Größe des Fehlers, kann jedoch ändern, ob bei einer Berechnung aufgrund des Fehlers ein Verlust oder ein Gewinn anfällt. Zum Beispiel:
b = 4.00000 / 7.00000;
b wäre 0,57143, was vom wahren Wert von 0,571428571 aufrundet ... Wären wir zu acht Stellen gegangen, wäre das 0,57142857, das eine weitaus geringere Fehlergröße aufweist, jedoch in die entgegengesetzte Richtung; es rundete ab.
Da sich durch Ändern der Genauigkeit ändern kann, ob ein Fehler ein Gewinn oder ein Verlust in jeder einzelnen Berechnung ist, kann sich dies ändern, ob sich die Fehler einer bestimmten Gesamtberechnung gegenseitig verstärken oder gegenseitig aufheben. Das Nettoergebnis ist, dass eine Berechnung mit niedrigerer Genauigkeit manchmal näher am "wahren" Ergebnis liegt als eine Berechnung mit höherer Genauigkeit, da Sie bei der Berechnung mit niedrigerer Genauigkeit Glück haben und die Fehler in verschiedene Richtungen gehen.
Ja, genau das passiert in Ihren Beispielen, außer dass wir anstelle von fünf Stellen mit Dezimalgenauigkeit eine bestimmte Anzahl von Stellen mit Binärgenauigkeit haben . So wie ein Drittel nicht in fünf oder einer endlichen Anzahl von Dezimalstellen genau dargestellt werden kann, können 0,1, 0,2 und 0,3 in keiner endlichen Anzahl von Binärziffern genau dargestellt werden. Einige davon werden aufgerundet, einige werden abgerundet, und ob Hinzufügungen den Fehler erhöhen oder den Fehler aufheben, hängt von den spezifischen Details der Anzahl der Binärziffern in jedem System ab. Das heißt, Änderungen in der Genauigkeit können die ändern Antwortwohl oder übel. Im Allgemeinen ist die Antwort umso näher an der wahren Antwort, je höher die Genauigkeit, aber nicht immer.
Wenn Sie eine genaue Dezimalrechnung benötigen, verwenden Sie den
decimal
Typ. Es werden Dezimalbrüche verwendet, keine binären Brüche. Der Preis, den Sie zahlen, ist, dass es erheblich größer und langsamer ist. Und natürlich werden, wie wir bereits gesehen haben, Brüche wie ein Drittel oder vier Siebtel nicht genau dargestellt. Jeder Bruch, der tatsächlich ein Dezimalbruch ist, wird jedoch mit einem Fehler von Null bis zu etwa 29 signifikanten Stellen dargestellt.Nein, Sie haben keine solche Garantie für Floats oder Double. Sowohl der Compiler als auch die Laufzeit dürfen Gleitkommaberechnungen mit höherer Genauigkeit durchführen, als dies in der Spezifikation vorgeschrieben ist. Insbesondere dürfen der Compiler und die Laufzeit Arithmetik mit einfacher Genauigkeit (32 Bit) in 64 Bit oder 80 Bit oder 128 Bit oder einer beliebigen Bitgröße von mehr als 32 Bit ausführen .
Der Compiler und die Laufzeit dürfen dies tun, wie sie sich gerade fühlen . Sie müssen nicht von Maschine zu Maschine, von Lauf zu Lauf usw. konsistent sein. Da dies nur die Berechnungen genauer machen kann, wird dies nicht als Fehler angesehen. Es ist eine Funktion. Eine Funktion, die es unglaublich schwierig macht, Programme zu schreiben, die sich vorhersehbar verhalten, aber dennoch eine Funktion.
Ja.
Da der erste vom Compiler und der zweite von der Laufzeit berechnet wird und ich nur gesagt habe, dass sie nach Belieben willkürlich mehr Präzision verwenden dürfen, als in der Spezifikation gefordert, können diese zu unterschiedlichen Ergebnissen führen. Vielleicht wählt einer von ihnen die Berechnung nur mit einer Genauigkeit von 64 Bit, während der andere eine Genauigkeit von 80 Bit oder 128 Bit für einen Teil oder die gesamte Berechnung auswählt und eine Differenzantwort erhält.
Richtig.
Die Art und Weise, wie dies normalerweise dem C # -Compilerteam gemeldet wird, ist, dass jemand einen Ausdruck hat, der beim Kompilieren im Debug-Modus true und beim Kompilieren im Release-Modus false erzeugt. Dies ist die häufigste Situation, in der dies auftritt, weil die Debug- und Release-Code-Generierung die Registerzuordnungsschemata ändert. Der Compiler darf mit diesem Ausdruck jedoch alles tun, was er möchte, solange er zwischen wahr und falsch wählt. (Es kann beispielsweise keinen Fehler bei der Kompilierung verursachen.)
Richtig.
Nicht ich, das ist verdammt sicher.
Intel entschied sich für einen Gleitkomma-Mathematikchip, bei dem es weitaus teurer war, konsistente Ergebnisse zu erzielen. Kleine Auswahlmöglichkeiten im Compiler, welche Operationen registriert werden sollen und welche Operationen auf dem Stapel bleiben sollen, können zu großen Unterschieden bei den Ergebnissen führen.
Verwenden Sie den
decimal
Typ, wie ich bereits sagte. Oder rechnen Sie alle in ganzen Zahlen.Ja. Wenn Sie ein Ergebnis in einem statischen Feld , einem Instanzfeld eines Klassen- oder Array-Elements vom Typ float oder double speichern , wird es garantiert auf eine Genauigkeit von 32 oder 64 Bit zurückgeschnitten. (Diese Garantie gilt ausdrücklich nicht für Geschäfte mit lokalen oder formalen Parametern.) Auch wenn Sie eine Laufzeitumwandlung in
(float)
oder(double)
auf einen Ausdruck durchführen, der bereits von diesem Typ ist, gibt der Compiler speziellen Code aus, der das Abschneiden des Ergebnisses erzwingt wurde einem Feld oder Array-Element zugewiesen. (Casts, die zur Kompilierungszeit ausgeführt werden, dh Casts auf konstante Ausdrücke, können dies nicht garantieren.)Nein. Die Laufzeit garantiert, dass das Speichern in einem Array oder Feld abgeschnitten wird. Die C # -Spezifikation garantiert nicht, dass eine Identitätsumwandlung abgeschnitten wird, aber die Microsoft-Implementierung verfügt über Regressionstests, die sicherstellen, dass jede neue Version des Compilers dieses Verhalten aufweist.
Die Sprachspezifikation zu diesem Thema besagt lediglich, dass Gleitkommaoperationen nach Ermessen der Implementierung mit höherer Genauigkeit ausgeführt werden können.
quelle
damn! my answer doesn't look logical anymore..
Wenn du schreibst
double a = 0.1d; double b = 0.2d; double c = 0.3d;
Eigentlich sind diese nicht genau
0.1
,0.2
und0.3
. Aus IL-Code;IL_0001: ldc.r8 0.10000000000000001 IL_000a: stloc.0 IL_000b: ldc.r8 0.20000000000000001 IL_0014: stloc.1 IL_0015: ldc.r8 0.29999999999999999
Es gibt eine Menge Fragen in SO, die auf dieses Problem hinweisen, wie ( Unterschied zwischen Dezimal, Gleitkomma und Doppel in .NET? Und Umgang mit Gleitkommafehlern in .NET ), aber ich empfehle Ihnen, den coolen Artikel mit dem Namen zu lesen.
What Every Computer Scientist Should Know About Floating-Point Arithmetic
Nun , was Leppie sagte, ist logischer. Die reale Situation ist hier, hängt ganz von
compiler
/computer
oder abcpu
.Basierend auf Leppie-Code funktioniert dieser Code in meinem Visual Studio 2010 und Linqpad als Ergebnis
True
/False
, aber wenn ich ihn auf ideone.com ausprobiert habe , ist das ErgebnisTrue
/True
Überprüfen Sie die DEMO .
Tipp : Als ich
Console.WriteLine(.1f + .2f == .3f);
Resharper schrieb, warnte mich;quelle
0.1f+0.2f==0.3f
wird sowohl im Debug- als auch im Release-Modus zu false kompiliert. Daher ist es für den Gleichheitsoperator falsch.Wie in den Kommentaren erwähnt, liegt dies daran, dass der Compiler eine konstante Weitergabe durchführt und die Berechnung mit einer höheren Genauigkeit durchführt (ich glaube, dies ist CPU-abhängig).
var f1 = .1f + .2f; var f2 = .3f; Console.WriteLine(f1 == f2); // prints true (same as Equals) Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)
@Caramiriel weist auch darauf hin, dass
.1f+.2f==.3f
die Ausgabe wiefalse
in der IL erfolgt, daher hat der Compiler die Berechnung zur Kompilierungszeit durchgeführt.Zur Bestätigung der ständigen Optimierung des Falt- / Ausbreitungs-Compilers
const float f1 = .1f + .2f; const float f2 = .3f; Console.WriteLine(f1 == f2); // prints false
quelle
Equals
Fall nicht die gleiche Optimierung durch ?(0.1d+.2d).Equals(.3d) == false
, weil ES IST!float
ist astruct
, daher kann es nicht in Unterklassen unterteilt werden. Und eine Float-Konstante hat auch eine ziemlich konstanteEquals
Implementierung.FWIW nach Testdurchläufen
float x = 0.1f + 0.2f; float result = 0.3f; bool isTrue = x.Equals(result); bool isTrue2 = x == result; Assert.IsTrue(isTrue); Assert.IsTrue(isTrue2);
Das Problem liegt also tatsächlich bei dieser Leitung
Was wie gesagt wahrscheinlich compiler / pc-spezifisch ist
Die meisten Leute springen bei dieser Frage aus einem falschen Blickwinkel, denke ich bisher
AKTUALISIEREN:
Ein weiterer merkwürdiger Test, denke ich
const float f1 = .1f + .2f; const float f2 = .3f; Assert.AreEqual(f1, f2); passes Assert.IsTrue(f1==f2); doesnt pass
Implementierung einer einheitlichen Gleichstellung:
public bool Equals(float obj) { return ((obj == this) || (IsNaN(obj) && IsNaN(this))); }
quelle
==
geht es darum, genaue Float-Werte zu vergleichen.Equals
ist eine boolesche Methode, die true oder false zurückgeben kann. Die spezifische Implementierung kann variieren.quelle
Ich weiß nicht warum, aber zu diesem Zeitpunkt unterscheiden sich einige meiner Ergebnisse von Ihren. Beachten Sie, dass der dritte und vierte Test dem Problem zuwiderlaufen, sodass Teile Ihrer Erklärungen jetzt möglicherweise falsch sind.
using System; class Test { static void Main() { float a = .1f + .2f; float b = .3f; Console.WriteLine(a == b); // true Console.WriteLine(a.Equals(b)); // true Console.WriteLine(.1f + .2f == .3f); // true Console.WriteLine((1f + .2f).Equals(.3f)); //false Console.WriteLine(.1d + .2d == .3d); //false Console.WriteLine((1d + .2d).Equals(.3d)); //false } }
quelle