Warum bleibt das mehrfache Hinzufügen von 0,1 verlustfrei?

152

Ich weiß, dass die 0.1Dezimalzahl nicht exakt mit einer endlichen Binärzahl dargestellt werden kann ( Erklärung ), daher double n = 0.1verliert sie an Genauigkeit und ist nicht genau 0.1. Andererseits 0.5kann genau dargestellt werden, weil es ist 0.5 = 1/2 = 0.1b.

Trotzdem ist es verständlich, dass 0.1 dreimaliges Hinzufügen nicht genau 0.3den folgenden Code ergibt false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Aber wie kommt es dann, dass 0.1 fünfmaliges Hinzufügen genau ergibt 0.5? Der folgende Code wird gedruckt true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Wenn 0.1es nicht genau dargestellt werden kann, wie kommt es dann, dass das 5-fache Hinzufügen genau 0.5das ergibt, was genau dargestellt werden kann?

icza
quelle
7
Wenn Sie es wirklich erforschen, können Sie es sicher herausfinden, aber Gleitkomma steckt voller "Überraschungen", und manchmal ist es besser, nur verwundert zuzusehen.
Hot Licks
3
Sie denken mathematisch darüber nach. Gleitkomma-Aritmetik ist in keiner Weise mathematisch.
Jakob
13
@HotLicks das ist sehr viel die falsche Einstellung zu haben.
Hobbs
2
@RussellBorogove, selbst wenn es wegoptimiert wurde, wäre es nur dann eine gültige Optimierung, wenn sumes den gleichen Endwert hätte, als ob die Schleife wirklich ausgeführt worden wäre. Im C ++ - Standard wird dies als "Als-ob-Regel" oder "gleiches beobachtbares Verhalten" bezeichnet.
Hobbs
7
@ Jakob überhaupt nicht wahr. Die Gleitkomma-Arithmetik ist streng definiert, mit einer guten mathematischen Behandlung von Fehlergrenzen und dergleichen. Es ist nur so, dass viele Programmierer entweder nicht bereit sind, die Analyse fortzusetzen, oder sie glauben fälschlicherweise, dass "Gleitkomma ungenau ist" alles ist, was man wissen muss, und dass es sich nicht lohnt, sich mit der Analyse zu beschäftigen.
Hobbs

Antworten:

155

Der Rundungsfehler ist nicht zufällig und versucht durch seine Implementierung, den Fehler zu minimieren. Dies bedeutet, dass der Fehler manchmal nicht sichtbar ist oder kein Fehler vorliegt.

Zum Beispiel 0.1ist nicht genau 0.1dh new BigDecimal("0.1") < new BigDecimal(0.1)aber 0.5genau1.0/2

Dieses Programm zeigt Ihnen die wahren Werte.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

druckt

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Hinweis: Das 0.3ist etwas falsch, aber wenn Sie zu 0.4den Bits kommen, müssen Sie eine nach unten verschieben, um in die 53-Bit-Grenze zu passen, und der Fehler wird verworfen. Wieder zurück ein Fehler einschleicht für 0.6und 0.7aber für 0.8zu 1.0den Fehlern verworfen.

Durch fünfmaliges Hinzufügen sollte der Fehler kumuliert und nicht abgebrochen werden.

Der Grund für einen Fehler liegt in der eingeschränkten Genauigkeit. dh 53-Bit. Dies bedeutet, dass, da die Anzahl mehr Bits verwendet, wenn sie größer wird, Bits am Ende entfernt werden müssen. Dies führt zu Rundungen, die in diesem Fall zu Ihren Gunsten sind.
Sie können den gegenteiligen Effekt erzielen, wenn Sie eine kleinere Zahl erhalten, z. B. 0.1-0.0999=>, 1.0000000000000286E-4 und Sie sehen mehr Fehler als zuvor.

Ein Beispiel hierfür ist, warum in Java 6 Math.round (0.49999999999999994) 1 zurückgibt. In diesem Fall führt der Verlust eines Bits bei der Berechnung zu einem großen Unterschied zur Antwort.

Peter Lawrey
quelle
1
Wo wird das umgesetzt?
EpicPandaForce
16
@Zhuinden Die CPU folgt dem IEEE-754-Standard. Java gibt Ihnen Zugriff auf die zugrunde liegenden CPU-Anweisungen und wird nicht beteiligt. en.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey
10
@ PeterLawrey: Nicht unbedingt die CPU. Auf einem Computer ohne Gleitkomma in der CPU (und ohne separate FPU) wird die IEEE-Arithmetik von der Software ausgeführt. Und wenn die Host-CPU Gleitkomma hat, aber nicht den IEEE-Anforderungen entspricht, wäre eine Java-Implementierung für diese CPU verpflichtet, auch Soft Float zu verwenden ...
R .. GitHub STOP HELPING ICE
1
@R .. In diesem Fall weiß ich nicht, was passieren würde, wenn Sie strictfp Time verwenden würden, um Festkomma-Ganzzahlen zu berücksichtigen, denke ich. (oder BigDecimal)
Peter Lawrey
2
@eugene Das Hauptproblem sind die begrenzten Werte, die Gleitkomma darstellen kann. Diese Einschränkung kann zu einem Informationsverlust und mit zunehmender Anzahl zu einem Fehlerverlust führen. Es wird gerundet, aber in diesem Fall wird abgerundet, sodass eine Zahl, die etwas zu groß gewesen wäre, da 0,1 etwas zu groß ist, zum richtigen Wert wird. Genau 0,5
Peter Lawrey
47

Der Sperrüberlauf im Gleitkomma x + x + xist genau die korrekt gerundete (dh der realen 3 * am nächsten liegende) Gleitkommazahl x, x + x + x + xist genau 4 * xund x + x + x + x + xist wiederum die korrekt gerundete Gleitkommanäherung für 5 * x.

Das erste Ergebnis x + x + xergibt sich aus der Tatsache, dass x + xes genau ist. x + x + xist somit das Ergebnis nur einer Rundung.

Das zweite Ergebnis ist schwieriger, eine Demonstration davon wird hier diskutiert (und Stephen Canon spielt auf einen anderen Beweis durch Fallanalyse auf den letzten 3 Ziffern von an x). Zusammenfassend ist entweder 3 * xin der gleichen Binade wie 2 * xoder es ist in der gleichen Binade wie 4 * x, und es ist jeweils möglich zu schließen, dass der Fehler bei der dritten Addition den Fehler bei der zweiten Addition aufhebt (die erste Hinzufügung genau, wie wir bereits sagten).

Das dritte Ergebnis, " x + x + x + x + xist korrekt gerundet", leitet sich vom zweiten auf dieselbe Weise ab wie das erste von der Genauigkeit von x + x.


Das zweite Ergebnis erklärt, warum 0.1 + 0.1 + 0.1 + 0.1genau die Gleitkommazahl ist 0.4: Die rationalen Zahlen 1/10 und 4/10 werden bei der Konvertierung in Gleitkommazahl auf dieselbe Weise mit demselben relativen Fehler angenähert. Diese Gleitkommazahlen haben ein Verhältnis von genau 4 zwischen ihnen. Das erste und dritte Ergebnis zeigen, dass 0.1 + 0.1 + 0.1und es 0.1 + 0.1 + 0.1 + 0.1 + 0.1ist zu erwarten, dass sie weniger Fehler aufweisen, als durch eine naive Fehleranalyse abgeleitet werden könnte, aber an sich beziehen sie sich nur auf die Ergebnisse bzw. 3 * 0.1und 5 * 0.1, von denen erwartet werden kann, dass sie nahe beieinander liegen, aber nicht unbedingt identisch mit 0.3und 0.5.

Wenn Sie 0.1nach der vierten Addition weiter hinzufügen , werden Sie schließlich Rundungsfehler feststellen, die 0.1dazu führen, dass " n-mal zu sich selbst hinzugefügt" n * 0.1von n / 10 abweicht und noch mehr von n / 10 abweicht. Wenn Sie die Werte von „0,1 n-mal zu sich selbst addiert“ als Funktion von n zeichnen würden, würden Sie Linien konstanter Steigung durch Binaden beobachten (sobald das Ergebnis der n-ten Addition dazu bestimmt ist, in eine bestimmte Binade zu fallen). Es ist zu erwarten, dass die Eigenschaften der Addition denen früherer Additionen ähnlich sind, die zu derselben Binade geführt haben. Innerhalb derselben Binade wächst oder schrumpft der Fehler. Wenn Sie sich die Reihenfolge der Steigungen von Binade zu Binade ansehen würden, würden Sie die sich wiederholenden Ziffern von erkennen0.1in binär für eine Weile. Danach würde die Absorption beginnen und die Kurve würde flach werden.

Pascal Cuoq
quelle
1
In der ersten Zeile sagen Sie, dass x + x + x genau richtig ist, aber aus dem Beispiel in der Frage ist es nicht.
Alboz
2
@Alboz Ich sage, das x + x + xist genau die korrekt gerundete Gleitkommazahl auf die reale 3 * x. "Richtig gerundet" bedeutet in diesem Zusammenhang "am nächsten".
Pascal Cuoq
4
+1 Dies sollte die akzeptierte Antwort sein. Es bietet tatsächlich Erklärungen / Beweise dafür, was vor sich geht, und nicht nur vage Allgemeingültigkeiten.
R .. GitHub STOP HELPING ICE
1
@Alboz (all das sieht die Frage vor). Diese Antwort erklärt jedoch, wie sich die Fehler zufällig aufheben, anstatt sich im schlimmsten Fall zu summieren.
Hobbs
1
@chebus 0.1 ist 0x1.999999999999999999999… p-4 in hexadezimaler Schreibweise (eine unendliche Folge von Ziffern). Es wird mit doppelter Genauigkeit als 0x1.99999ap-4 angenähert. 0,2 ist 0x1,99999999999999999999999… p-3 hexadezimal. Aus dem gleichen Grund, dass 0,1 als 0x1,99999ap-4 angenähert wird, wird 0,2 als 0x1,99999ap-3 angenähert. Inzwischen ist 0x1.99999ap-3 auch genau 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq
-1

Gleitkommasysteme zaubern auf verschiedene Weise, einschließlich einiger zusätzlicher Präzision beim Runden. Somit wird der sehr kleine Fehler aufgrund der ungenauen Darstellung von 0,1 auf 0,5 gerundet.

Stellen Sie sich Gleitkomma als eine großartige, aber ungenaue Möglichkeit vor, Zahlen darzustellen. Nicht alle möglichen Zahlen lassen sich in einem Computer leicht darstellen. Irrationale Zahlen wie PI. Oder wie SQRT (2). (Symbolische mathematische Systeme können sie darstellen, aber ich habe "leicht" gesagt.)

Der Gleitkommawert kann extrem nahe, aber nicht genau sein. Es kann so nah sein, dass Sie zu Pluto navigieren und um Millimeter abweichen können. Aber im mathematischen Sinne immer noch nicht genau.

Verwenden Sie kein Gleitkomma, wenn Sie genau und nicht ungefähr sein müssen. Beispielsweise möchten Buchhaltungsanwendungen eine bestimmte Anzahl von Pennys in einem Konto genau verfolgen. Ganzzahlen sind dafür gut, weil sie genau sind. Das Hauptproblem, auf das Sie bei Ganzzahlen achten müssen, ist der Überlauf.

Die Verwendung von BigDecimal für Währungen funktioniert gut, da die zugrunde liegende Darstellung eine Ganzzahl ist, wenn auch eine große.

In der Erkenntnis, dass Gleitkommazahlen ungenau sind, haben sie immer noch sehr viele Verwendungsmöglichkeiten. Koordinatensysteme für die Navigation oder Koordinaten in Grafiksystemen. Astronomische Werte. Wissenschaftliche Werte. (Sie können wahrscheinlich die genaue Masse eines Baseballs ohnehin nicht innerhalb einer Masse eines Elektrons kennen, daher spielt Ungenauigkeit keine Rolle.)

Verwenden Sie zum Zählen von Anwendungen (einschließlich Buchhaltung) eine Ganzzahl. Verwenden Sie int oder long, um die Anzahl der Personen zu zählen, die durch ein Tor gehen.

DannyB
quelle
2
Die Frage ist mit [Java] markiert. Die Java-Sprachdefinition sieht keine "wenigen zusätzlichen Genauigkeitsbits" vor, sondern nur einige wenige zusätzliche Exponentenbits (und das nur, wenn Sie diese nicht verwenden strictfp). Nur weil Sie darauf verzichtet haben, etwas zu verstehen, heißt das nicht, dass es unergründlich ist, und dass andere darauf verzichten sollten, es zu verstehen. Unter stackoverflow.com/questions/18496560 finden Sie ein Beispiel für die Länge, in der Java-Implementierungen ausgeführt werden, um die Sprachdefinition zu implementieren (die weder Bestimmungen für Bits mit zusätzlicher Genauigkeit noch mit strictfpzusätzlichen Bits enthält)
Pascal Cuoq