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 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.
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.3
mit (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.3
kann anders sein als (0.1 + 0.2).Equals(0.3)
. Sie sagen, dass 0.1 + 0.2 == 0.3
dies 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 decimal
Typ, 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.