(.1f + .2f ==. 3f)! = (.1f + .2f) .Gleich (.3f) Warum?

68

Bei meiner Frage geht es nicht um schwebende Präzision. Es geht darum, warum Equals()anders ist als ==.

Ich verstehe warum .1f + .2f == .3fist false(während .1m + .2m == .3mist 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
LZW
quelle
Diese Frage enthält weitere Details zu den Unterschieden zwischen Gleitkomma- und Dezimaltypen.
Freundlich
Nur fürs Protokoll, keine wirkliche Antwort: Math.Abs(.1d + .2d - .3d) < double.EpsilonDies sollte die bessere Gleichstellungsmethode sein.
Malmi
10
Zu Ihrer Information ==ist nicht „Referenz“ Vergleich und .Equals()ist nicht „Wert“ Vergleich. Ihre Implementierung ist typspezifisch.
Chris Sinclair
15
Nur zur Verdeutlichung: Der Unterschied besteht darin, dass es sich im ersten Fall 0.1 + 0.2 == 0.3um einen konstanten Ausdruck handelt , der zur Kompilierungszeit vollständig berechnet werden kann. In (0.1 + 0.2).Equals(0.3)der 0.1 + 0.2und die 0.3sind alle konstante Ausdrücke aber die Gleichheit wird durch die Laufzeit berechnet wird , nicht durch den Compiler. Ist das klar?
Eric Lippert
8
Nur um wählerisch zu sein: Die Unterschiede, die dazu führen, dass die Berechnung mit höherer Genauigkeit durchgeführt wird, müssen nicht "Umwelt" sein. Sowohl der Compiler als auch die Laufzeit dürfen unabhängig von Umgebungsdetails aus irgendeinem Grund eine höhere Genauigkeit verwenden . In der Praxis hängt die Entscheidung, wann eine höhere Genauigkeit gegenüber einer niedrigeren Genauigkeit verwendet werden soll, normalerweise von der Verfügbarkeit der Register ab. Registrierte Ausdrücke haben eine höhere Genauigkeit.
Eric Lippert

Antworten:

133

Die Frage ist verwirrend formuliert. Lassen Sie es uns in viele kleinere Fragen aufteilen:

Warum entspricht ein Zehntel plus zwei Zehntel in der Gleitkomma-Arithmetik nicht immer drei Zehnteln?

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

q = x + x + x + x;
r = y + x + x;
s = y + y;

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.

Hat die Anzahl der Genauigkeitsstellen Einfluss auf die Größe und Richtung des Fehlers?

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.

Wir würden erwarten, dass eine Berechnung mit höherer Genauigkeit immer eine Antwort liefert, die der wahren Antwort näher kommt, aber dieses Argument zeigt etwas anderes. Dies erklärt, warum manchmal eine Berechnung in Floats die "richtige" Antwort liefert, aber eine Berechnung in Doppel - die die doppelte Genauigkeit haben - die "falsche" Antwort liefert, richtig?

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.

Wie kann ich dann genaue dezimale arithmetische Berechnungen erhalten, wenn float und double Binärziffern verwenden?

Wenn Sie eine genaue Dezimalrechnung benötigen, verwenden Sie den decimalTyp. 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.

OK, ich akzeptiere, dass alle Gleitkommaschemata aufgrund von Darstellungsfehlern Ungenauigkeiten verursachen und dass sich diese Ungenauigkeiten manchmal aufgrund der Anzahl der in der Berechnung verwendeten Genauigkeitsbits akkumulieren oder aufheben können. Haben wir zumindest die Garantie, dass diese Ungenauigkeiten konsistent sind ?

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.

Das bedeutet also, dass zur Kompilierungszeit durchgeführte Berechnungen wie die Literale 0.1 + 0.2 andere Ergebnisse liefern können als dieselbe zur Laufzeit mit Variablen durchgeführte Berechnung?

Ja.

Was ist mit dem Vergleich der Ergebnisse von 0.1 + 0.2 == 0.3mit (0.1 + 0.2).Equals(0.3)?

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.

Also warte eine Minute hier. Sie sagen, nicht nur das 0.1 + 0.2 == 0.3kann anders sein als (0.1 + 0.2).Equals(0.3). Sie sagen, dass 0.1 + 0.2 == 0.3dies nach Lust und Laune des Compilers als wahr oder falsch berechnet werden kann. Es könnte dienstags wahr und donnerstags falsch erzeugen, es könnte auf einer Maschine wahr und auf einer anderen falsch erzeugen, es könnte sowohl wahr als auch falsch erzeugen, wenn der Ausdruck zweimal im selben Programm erscheint. Dieser Ausdruck kann aus irgendeinem Grund einen beliebigen Wert haben. Der Compiler darf hier völlig unzuverlässig sein.

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.)

Das ist Verrücktheit.

Richtig.

Wen sollte ich für dieses Durcheinander verantwortlich machen?

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.

Wie stelle ich konsistente Ergebnisse sicher?

Verwenden Sie den decimalTyp, wie ich bereits sagte. Oder rechnen Sie alle in ganzen Zahlen.

Ich muss Doppel- oder Schwimmkörper verwenden. Kann ich etwas tun , um konsistente Ergebnisse zu fördern?

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.)

Um diesen letzten Punkt zu verdeutlichen: Gibt die C # -Sprachenspezifikation diese Garantien?

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.

Eric Lippert
quelle
1
Problem tritt auf, wenn wir bool result = 0.1f + 0.2f == 0.3f zuweisen. Wenn wir 0.1f + 0.2f nicht in einer Variablen speichern, erhalten wir false. Wenn wir 0.1f + 0.2f in einer Variablen speichern, erhalten wir true. Es hat wenig mit allgemeiner Gleitkomma-Arithmetik zu tun, wenn überhaupt, die Hauptfrage hier ist, warum bool x = 0,1f + 0,2f == 0,3f falsch ist, aber float temp = 0,1f + 0,2f; bool x = temp == 0.3f ist wahr, Rest ist üblicher Gleitkomma-
Frageteil
14
Wenn Eric Lippert die gleiche Frage mit mir beantwortete, habe ich immer das Gefühldamn! my answer doesn't look logical anymore..
Soner Gönül
5
Ich weiß es wirklich zu schätzen, dass Sie sich immer noch die Zeit nehmen und die Geduld haben, einen so sorgfältig geschriebenen und ziemlich langen Beitrag für eine Frage beizusteuern, die wahrscheinlich einmal pro Woche auftaucht. +1
Groo
20
@ MarkHurd: Ich denke, Sie bekommen nicht die volle Wirkung von dem, was ich hier sage. Es geht nicht darum, was der C # -Compiler oder der VB-Compiler macht. Der C # -Compiler kann diese Frage jederzeit aus irgendeinem Grund beantworten . Sie können dasselbe Programm zweimal kompilieren und unterschiedliche Antworten erhalten. Sie können die Frage zweimal im selben Programm stellen und zwei verschiedene Antworten erhalten. C # und VB erzeugen nicht "die gleichen Ergebnisse", da C # und C # nicht unbedingt die gleichen Ergebnisse erzeugen. Wenn sie zufällig die gleichen Ergebnisse erzielen, ist das ein glücklicher Zufall.
Eric Lippert
5
Was für eine Antwort. Deshalb benutze ich StackOverflow.
Sid Holland
8

Wenn du schreibst

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;

Eigentlich sind diese nicht genau 0.1, 0.2und 0.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/ computeroder ab cpu.

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 Ergebnis True/True

Überprüfen Sie die DEMO .

Tipp : Als ich Console.WriteLine(.1f + .2f == .3f);Resharper schrieb, warnte mich;

Vergleich der Gleitkommazahl mit dem Gleichheitsoperator. Möglicher Genauigkeitsverlust beim Runden von Werten.

Geben Sie hier die Bildbeschreibung ein

Soner Gönül
quelle
Er fragt nach dem Fall mit einfacher Präzision. Es gibt kein Problem mit einem Fall mit doppelter Genauigkeit.
Leppie
1
Anscheinend gibt es einen Unterschied zwischen dem Code, der ausgeführt wird, und dem Compiler. 0.1f+0.2f==0.3fwird sowohl im Debug- als auch im Release-Modus zu false kompiliert. Daher ist es für den Gleichheitsoperator falsch.
Caramiriel
6

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==.3fdie Ausgabe wie falsein 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
Leppie
quelle
Aber warum wird im letzten Fall nicht die gleiche Optimierung durchgeführt?
Groo
2
@ SonerGönül: Bald von seiner Hoheit verdunkelt zu werden; p Danke
Leppie
Ok, lassen Sie es mich klarer formulieren, da ich mich auf den letzten Fall von OP bezog: Aber warum führt es in diesem EqualsFall nicht die gleiche Optimierung durch ?
Groo
@Groo: wenn du meinst (0.1d+.2d).Equals(.3d) == false, weil ES IST!
Leppie
1
@ njzk2: Nun, floatist a struct, daher kann es nicht in Unterklassen unterteilt werden. Und eine Float-Konstante hat auch eine ziemlich konstante EqualsImplementierung.
Groo
2

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

0,1f + 0,2f == 0,3f

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)));
}
Valentin Kuzub
quelle
Ich stimme Ihrer letzten Aussage zu :)
Leppie
@leppie hat meine Antwort mit einem neuen Test aktualisiert. Kannst du mir sagen, warum der erste Durchgang und der zweite nicht. Ich verstehe nicht ganz, angesichts der Implementierung von Equals
Valentin Kuzub
0

== geht es darum, genaue Float-Werte zu vergleichen.

Equalsist eine boolesche Methode, die true oder false zurückgeben kann. Die spezifische Implementierung kann variieren.

njzk2
quelle
Überprüfen Sie meine Antwort für die Implementierung von float Equals. Der tatsächliche Unterschied besteht darin, dass equals zur Laufzeit ausgeführt wird, während == zur Kompilierungszeit ausgeführt werden kann. == ist auch eine "boolesche Methode" (ich habe mehr über boolesche Funktionen gehört), praktisch
Valentin Kuzub
0

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
    }
}
imba-tjd
quelle