Ist die Gleitkomma-Mathematik kaputt?

2983

Betrachten Sie den folgenden Code:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

Warum treten diese Ungenauigkeiten auf?

Cato Johnston
quelle
127
Gleitkommavariablen haben normalerweise dieses Verhalten. Dies liegt daran, wie sie in der Hardware gespeichert sind. Weitere Informationen finden Sie im Wikipedia-Artikel zu Gleitkommazahlen .
Ben S
62
JavaScript behandelt Dezimalstellen als Gleitkommazahlen , was bedeutet, dass Operationen wie das Hinzufügen möglicherweise Rundungsfehler aufweisen. Vielleicht möchten Sie einen Blick auf diesen Artikel werfen: Was jeder Informatiker über Gleitkomma-Arithmetik wissen sollte
matt b
4
Nur zur Information, ALLE numerischen Typen in Javascript sind IEEE-754 Doubles.
Gary Willoughby
6
Da JavaScript den IEEE 754-Standard für Mathematik verwendet, werden schwebende 64-Bit- Zahlen verwendet. Dies führt zu Präzisionsfehlern bei Gleitkommaberechnungen (Dezimalzahlen), kurz gesagt, weil Computer in Basis 2 arbeiten, während Dezimalzahl Basis 10 ist .
Pardeep Jain

Antworten:

2252

Binäre Gleitkomma- Mathematik ist so. In den meisten Programmiersprachen basiert es auf dem IEEE 754-Standard . Der Kern des Problems besteht darin, dass Zahlen in diesem Format als ganze Zahl mal Zweierpotenz dargestellt werden. rationale Zahlen (wie 0.1, das ist 1/10) , dessen Nenner ist nicht genau eine Zweierpotenz nicht dargestellt werden kann.

Denn 0.1im Standardformat binary64kann die Darstellung genau so geschrieben werden

  • 0.1000000000000000055511151231257827021181583404541015625 in Dezimalzahl oder
  • 0x1.999999999999ap-4in C99 Hexfloat-Notation .

Im Gegensatz dazu die rationale Zahl 0.1, das ist 1/10kann geschrieben werden , genau wie

  • 0.1 in Dezimalzahl oder
  • 0x1.99999999999999...p-4in einem Analogon der C99-Hexfloat-Notation, wobei das ...eine endlose Folge von 9en darstellt.

Die Konstanten 0.2und 0.3in Ihrem Programm sind auch Annäherungen an ihre wahren Werte. Es kommt vor, dass die am nächsten doublezu 0.2größer als die rationalen Zahl 0.2aber , dass die am nächsten doublezu 0.3kleiner ist als die rationale Zahl 0.3. Die Summe von 0.1und 0.2wird größer als die rationale Zahl 0.3und stimmt daher nicht mit der Konstante in Ihrem Code überein.

Eine ziemlich umfassende Behandlung von Gleitkomma-Arithmetikproblemen sollte jeder Informatiker über Gleitkomma-Arithmetik wissen . Eine leicht verständliche Erklärung finden Sie unter float-point-gui.de .

Randnotiz: Alle Positionsnummernsysteme (Basis-N) teilen dieses Problem mit Präzision

Einfache alte Dezimalzahlen (Basis 10) haben dieselben Probleme, weshalb Zahlen wie 1/3 als 0,333333333 enden ...

Sie sind gerade auf eine Zahl (3/10) gestoßen, die mit dem Dezimalsystem leicht darzustellen ist, aber nicht zum Binärsystem passt. Es geht auch in beide Richtungen (bis zu einem gewissen Grad): 1/16 ist eine hässliche Zahl in Dezimalzahl (0,0625), aber in Binärform sieht es genauso ordentlich aus wie eine 10.000ste in Dezimalzahl (0,0001) ** - wenn wir in wären Als Gewohnheit, in unserem täglichen Leben ein Basis-2-Zahlensystem zu verwenden, würden Sie sich diese Zahl sogar ansehen und instinktiv verstehen, dass Sie dort ankommen könnten, indem Sie etwas halbieren, es immer wieder halbieren.

** Natürlich werden Gleitkommazahlen nicht genau so im Speicher gespeichert (sie verwenden eine Form der wissenschaftlichen Notation). Es zeigt jedoch, dass binäre Gleitkomma-Präzisionsfehler häufig auftreten, weil die Zahlen der "realen Welt", mit denen wir normalerweise arbeiten möchten, so oft Zehnerpotenzen sind - aber nur, weil wir einen Tag mit einem Dezimalzahlensystem verwenden. heute. Dies ist auch der Grund, warum wir Dinge wie 71% anstelle von "5 von 7" sagen (71% ist eine Annäherung, da 5/7 nicht genau mit einer Dezimalzahl dargestellt werden kann).

Also nein: binäre Gleitkommazahlen sind nicht gebrochen, sie sind einfach so unvollkommen wie jedes andere Basis-N-Zahlensystem :)

Seite Seite Hinweis: Arbeiten mit Floats in der Programmierung

In der Praxis bedeutet dieses Präzisionsproblem, dass Sie Rundungsfunktionen verwenden müssen, um Ihre Gleitkommazahlen auf die gewünschten Dezimalstellen abzurunden, bevor Sie sie anzeigen.

Sie müssen auch Gleichheitstests durch Vergleiche ersetzen, die ein gewisses Maß an Toleranz zulassen. Dies bedeutet:

Sie nicht tunif (x == y) { ... }

Stattdessen tun if (abs(x - y) < myToleranceValue) { ... }.

wo absist der absolute Wert. myToleranceValuemuss für Ihre spezielle Anwendung ausgewählt werden - und es hat viel damit zu tun, wie viel "Spielraum" Sie bereit sind, zuzulassen, und was die größte Anzahl ist, die Sie vergleichen werden (aufgrund von Präzisionsverlustproblemen) ). Achten Sie auf Konstanten im "Epsilon" -Stil in der Sprache Ihrer Wahl. Diese dürfen nicht als Toleranzwerte verwendet werden.

Daniel Scott
quelle
181
Ich denke, "eine Fehlerkonstante" ist korrekter als "The Epsilon", da es kein "The Epsilon" gibt, das in allen Fällen verwendet werden könnte. Unterschiedliche Epsilons müssen in unterschiedlichen Situationen verwendet werden. Und die Maschine epsilon ist fast nie eine gute Konstante.
Rotsor
34
Es ist nicht ganz richtig, dass alle Gleitkomma-Mathematik auf dem IEEE [754] -Standard basiert. Es werden immer noch einige Systeme verwendet, die beispielsweise das alte hexadezimale IBM-FP haben, und es gibt immer noch Grafikkarten, die keine IEEE-754-Arithmetik unterstützen. Es ist jedoch eine vernünftige Annäherung.
Stephen Canon
19
Cray hat die IEEE-754-Konformität aus Gründen der Geschwindigkeit aufgegeben. Java hat seine Einhaltung auch als Optimierung gelockert.
Art Taylor
28
Ich denke, Sie sollten dieser Antwort etwas hinzufügen, wie Berechnungen mit Geld immer und immer mit Festkomma-Arithmetik für ganze Zahlen durchgeführt werden sollten , da Geld quantisiert wird. (Es kann sinnvoll sein, interne Buchhaltungsberechnungen in winzigen Bruchteilen eines Cent durchzuführen, oder was auch immer Ihre kleinste Währungseinheit ist - dies hilft häufig beispielsweise bei der Reduzierung von Rundungsfehlern bei der Umrechnung von "29,99 USD pro Monat" in einen Tagessatz - sollte dies jedoch tun noch Festkomma-Arithmetik sein.)
zwol
18
Interessante Tatsache: Diese 0,1, die nicht genau im binären Gleitkomma dargestellt wird, verursachte einen berüchtigten Fehler in der Patriot-Raketensoftware, bei dem 28 Menschen während des ersten Irak-Krieges getötet wurden.
HDL
602

Die Perspektive eines Hardware-Designers

Ich glaube, ich sollte dem die Perspektive eines Hardware-Designers hinzufügen, da ich Gleitkomma-Hardware entwerfe und baue. Wenn Sie die Ursache des Fehlers kennen, können Sie besser verstehen, was in der Software geschieht, und ich hoffe, dass dies letztendlich dazu beiträgt, die Gründe zu erklären, warum Gleitkommafehler auftreten und sich im Laufe der Zeit zu akkumulieren scheinen.

1. Übersicht

Aus technischer Sicht weisen die meisten Gleitkommaoperationen ein Fehlerelement auf, da die Hardware, die die Gleitkommaberechnungen durchführt, an letzter Stelle nur einen Fehler von weniger als der Hälfte einer Einheit aufweisen muss. Daher stoppt viel Hardware bei einer Genauigkeit, die nur erforderlich ist, um einen Fehler von weniger als der Hälfte einer Einheit an letzter Stelle für eine einzelne Operation zu ergeben, was besonders bei der Gleitkommadivision problematisch ist. Was eine einzelne Operation ausmacht, hängt davon ab, wie viele Operanden die Einheit benötigt. Für die meisten sind es zwei, aber einige Einheiten benötigen 3 oder mehr Operanden. Aus diesem Grund gibt es keine Garantie dafür, dass wiederholte Vorgänge zu einem wünschenswerten Fehler führen, da sich die Fehler im Laufe der Zeit summieren.

2. Standards

Die meisten Prozessoren folgen dem IEEE-754- Standard, einige verwenden jedoch denormalisierte oder andere Standards. Beispielsweise gibt es in IEEE-754 einen denormalisierten Modus, der die Darstellung sehr kleiner Gleitkommazahlen auf Kosten der Genauigkeit ermöglicht. Im Folgenden wird jedoch der normalisierte Modus von IEEE-754 behandelt, der der typische Betriebsmodus ist.

Im IEEE-754-Standard dürfen Hardwareentwickler jeden Fehler- / Epsilon-Wert verwenden, solange er zuletzt weniger als die Hälfte einer Einheit beträgt und das Ergebnis nur weniger als die Hälfte einer Einheit am letzten sein muss Platz für eine Operation. Dies erklärt, warum sich die Fehler bei wiederholten Vorgängen summieren. Für die doppelte Genauigkeit des IEEE-754 ist dies das 54. Bit, da 53 Bits verwendet werden, um den numerischen Teil (normalisiert), auch Mantisse genannt, der Gleitkommazahl (z. B. 5,3 in 5,3e5) darzustellen. In den nächsten Abschnitten werden die Ursachen von Hardwarefehlern bei verschiedenen Gleitkommaoperationen ausführlicher beschrieben.

3. Ursache des Rundungsfehlers in der Division

Die Hauptursache für den Fehler bei der Gleitkommadivision sind die zur Berechnung des Quotienten verwendeten Divisionsalgorithmen. Die meisten Computersysteme berechnen die Division durch Multiplikation mit einer Inversen, hauptsächlich in Z=X/Y,Z = X * (1/Y). Eine Division wird iterativ berechnet, dh jeder Zyklus berechnet einige Bits des Quotienten, bis die gewünschte Genauigkeit erreicht ist, was für IEEE-754 alles ist, was an letzter Stelle einen Fehler von weniger als einer Einheit aufweist. Die Tabelle der Kehrwerte von Y (1 / Y) ist als Quotientenauswahltabelle (QST) in der langsamen Division bekannt, und die Größe in Bits der Quotientenauswahltabelle ist normalerweise die Breite des Radix oder eine Anzahl von Bits von der in jeder Iteration berechnete Quotient plus einige Schutzbits. Für den IEEE-754-Standard mit doppelter Genauigkeit (64 Bit) wäre dies die Größe des Radix des Teilers plus einige Schutzbits k, wobei k>=2. So wäre beispielsweise eine typische Quotientenauswahltabelle für einen Teiler, der jeweils 2 Bits des Quotienten berechnet (Radix 4), 2+2= 4Bits (plus einige optionale Bits).

3.1 Teilungsrundungsfehler: Approximation des Kehrwerts

Welche Kehrwerte in der Quotientenauswahltabelle enthalten sind, hängt von der Teilungsmethode ab : langsame Teilung wie SRT-Teilung oder schnelle Teilung wie Goldschmidt-Teilung; Jeder Eintrag wird gemäß dem Divisionsalgorithmus modifiziert, um den geringstmöglichen Fehler zu erzielen. In jedem Fall sind jedoch alle Kehrwerte Näherungswertedes tatsächlichen Kehrwerts und führen ein Element des Fehlers ein. Sowohl langsame als auch schnelle Teilungsmethoden berechnen den Quotienten iterativ, dh eine bestimmte Anzahl von Bits des Quotienten wird in jedem Schritt berechnet, dann wird das Ergebnis von der Dividende subtrahiert und der Teiler wiederholt die Schritte, bis der Fehler weniger als die Hälfte von eins beträgt Einheit an letzter Stelle. Langsame Teilungsmethoden berechnen eine feste Anzahl von Stellen des Quotienten in jedem Schritt und sind normalerweise kostengünstiger zu erstellen, und schnelle Teilungsmethoden berechnen eine variable Anzahl von Stellen pro Schritt und sind normalerweise teurer zu erstellen. Der wichtigste Teil der Teilungsmethoden besteht darin, dass die meisten von ihnen auf wiederholter Multiplikation durch Annäherung an einen Kehrwert beruhen , sodass sie fehleranfällig sind.

4. Rundungsfehler bei anderen Vorgängen: Abschneiden

Eine weitere Ursache für die Rundungsfehler bei allen Operationen sind die unterschiedlichen Kürzungsmodi der endgültigen Antwort, die IEEE-754 zulässt. Es gibt abgeschnitten, auf Null gerundet, auf den nächsten gerundet (Standard), abgerundet und aufgerundet. Alle Methoden führen an letzter Stelle für eine einzelne Operation ein Fehlerelement von weniger als einer Einheit ein. Im Laufe der Zeit und bei wiederholten Vorgängen trägt das Abschneiden auch kumulativ zum resultierenden Fehler bei. Dieser Kürzungsfehler ist besonders problematisch bei der Exponentiation, die eine Form der wiederholten Multiplikation beinhaltet.

5. Wiederholte Operationen

Da die Hardware, die die Gleitkommaberechnungen durchführt, nur ein Ergebnis mit einem Fehler von weniger als der Hälfte einer Einheit an letzter Stelle für eine einzelne Operation liefern muss, wächst der Fehler bei wiederholten Operationen, wenn sie nicht beobachtet wird. Dies ist der Grund dafür, dass Mathematiker bei Berechnungen, die einen begrenzten Fehler erfordern, Methoden verwenden, z. B. die Verwendung der auf die nächste gerade geraden Stelle an der letzten Stelle von IEEE-754, da sich die Fehler im Laufe der Zeit eher gegenseitig aufheben out und Intervallarithmetik kombiniert mit Variationen der IEEE 754-RundungsmodiRundungsfehler vorherzusagen und zu korrigieren. Aufgrund seines geringen relativen Fehlers im Vergleich zu anderen Rundungsmodi ist das Runden auf die nächste gerade Ziffer (an letzter Stelle) der Standardrundungsmodus von IEEE-754.

Beachten Sie, dass der Standardrundungsmodus, der an letzter Stelle auf die nächste gerade Zahl gerundet wird , einen Fehler von weniger als der Hälfte einer Einheit an letzter Stelle für eine Operation garantiert. Die Verwendung der Kürzung, Aufrundung und Abrundung allein kann zu einem Fehler führen, der mehr als die Hälfte einer Einheit an der letzten Stelle, aber weniger als eine Einheit an der letzten Stelle beträgt. Daher werden diese Modi nur empfohlen, wenn dies der Fall ist wird in der Intervallarithmetik verwendet.

6. Zusammenfassung

Kurz gesagt, der Hauptgrund für die Fehler bei Gleitkommaoperationen ist eine Kombination aus dem Abschneiden in der Hardware und dem Abschneiden eines Kehrwerts im Fall der Division. Da der IEEE-754-Standard nur einen Fehler von weniger als der Hälfte einer Einheit an letzter Stelle für eine einzelne Operation erfordert, addieren sich die Gleitkommafehler bei wiederholten Operationen, sofern sie nicht korrigiert werden.

KernelPanik
quelle
8
(3) ist falsch. Der Rundungsfehler in einer Division beträgt nicht weniger als eine Einheit an der letzten Stelle, aber höchstens eine halbe Einheit an der letzten Stelle.
Gnasher729
6
@ gnasher729 Guter Fang. Die meisten Grundoperationen haben auch einen Fehler von weniger als der Hälfte einer Einheit an letzter Stelle unter Verwendung des Standard-IEEE-Rundungsmodus. Die Erklärung wurde bearbeitet und es wurde auch festgestellt, dass der Fehler größer als die Hälfte eines Ulps, aber kleiner als 1 Ulp sein kann, wenn der Benutzer den Standardrundungsmodus überschreibt (dies gilt insbesondere für eingebettete Systeme).
KernelPanik
39
(1) Gleitkomma - Zahlen haben keine Fehler. Jeder Gleitkommawert ist genau das, was er ist. Die meisten (aber nicht alle) Gleitkomma - Operationen geben ungenaue Ergebnisse. Beispielsweise gibt es keinen binären Gleitkommawert, der genau 1,0 / 10,0 entspricht. Einige Operationen (zB 1.0 + 1.0) Sie geben genaue Ergebnisse auf der anderen Seite.
Solomon Slow
19
"Die Hauptursache für den Fehler bei der Gleitkommadivision sind die zur Berechnung des Quotienten verwendeten Divisionsalgorithmen" ist eine sehr irreführende Aussage. Bei einer IEEE-754-konformen Division ist die einzige Fehlerursache bei der Gleitkommadivision die Unfähigkeit, das Ergebnis im Ergebnisformat genau darzustellen. Das gleiche Ergebnis wird unabhängig vom verwendeten Algorithmus berechnet.
Stephen Canon
6
@ Matt Entschuldigung für die späte Antwort. Dies ist im Wesentlichen auf Ressourcen- / Zeitprobleme und Kompromisse zurückzuführen. Es gibt eine Möglichkeit, eine lange Teilung / mehr "normale" Teilung durchzuführen. Sie wird als SRT-Teilung mit Radix zwei bezeichnet. Dies verschiebt und subtrahiert jedoch wiederholt den Divisor von der Dividende und benötigt viele Taktzyklen, da nur ein Bit des Quotienten pro Taktzyklus berechnet wird. Wir verwenden Tabellen mit Hin- und Herbewegungen, damit wir mehr Bits des Quotienten pro Zyklus berechnen und effektive Kompromisse zwischen Leistung und Geschwindigkeit eingehen können.
KernelPanik
462

Wenn Sie .1 oder 1/10 in Basis 2 (binär) konvertieren, erhalten Sie ein sich wiederholendes Muster nach dem Dezimalpunkt, genau wie beim Versuch, 1/3 in Basis 10 darzustellen. Der Wert ist nicht genau und kann daher nicht verwendet werden exakte Mathematik mit normalen Gleitkomma-Methoden.

Joel Coehoorn
quelle
133
Tolle und kurze Antwort. Das sich wiederholende Muster sieht aus wie 0,00011001100110011001100110011001100110011001100110011 ...
Konstantin Chernov
4
Dies erklärt nicht, warum kein besserer Algorithmus verwendet wird, der überhaupt nicht in Binärdateien konvertiert wird.
Dmitri Zaitsev
12
Weil Leistung. Die Verwendung von Binärdateien ist einige tausend Mal schneller, da sie für die Maschine nativ sind.
Joel Coehoorn
7
Es gibt Methoden, die genaue Dezimalwerte liefern. BCD (Binary Coded Decimal) oder verschiedene andere Formen der Dezimalzahl. Diese sind jedoch beide langsamer (viel langsamer) und benötigen mehr Speicher als die Verwendung von binären Gleitkommazahlen. (In einem gepackten BCD werden beispielsweise 2 Dezimalstellen in einem Byte gespeichert. Das sind 100 mögliche Werte in einem Byte, in dem 256 mögliche Werte gespeichert werden können, oder 100/256, wodurch etwa 60% der möglichen Werte eines Bytes verschwendet werden.)
Duncan C
16
@Jacksonkr du denkst immer noch in Base-10. Computer sind Basis-2.
Joel Coehoorn
306

Die meisten Antworten hier beantworten diese Frage in sehr trockenen, technischen Begriffen. Ich möchte dies mit Begriffen ansprechen, die normale Menschen verstehen können.

Stellen Sie sich vor, Sie versuchen, Pizzen in Scheiben zu schneiden. Sie haben einen Roboter-Pizzaschneider, der Pizzastücke genau halbieren kann . Es kann eine ganze Pizza halbieren, oder es kann ein vorhandenes Stück halbieren, aber in jedem Fall ist die Halbierung immer genau.

Dieser Pizzaschneider hat sehr feine Bewegungen. Wenn Sie mit einer ganzen Pizza beginnen, diese halbieren und jedes Mal die kleinste Scheibe halbieren, können Sie die Halbierung 53 Mal durchführen, bevor die Scheibe selbst für ihre hochpräzisen Fähigkeiten zu klein ist . Ab diesem Zeitpunkt können Sie diese sehr dünne Schicht nicht mehr halbieren, sondern müssen sie unverändert ein- oder ausschließen.

Wie würden Sie nun alle Scheiben so zusammensetzen, dass sich ein Zehntel (0,1) oder ein Fünftel (0,2) einer Pizza ergibt? Denken Sie wirklich darüber nach und versuchen Sie es herauszufinden. Sie können sogar versuchen, eine echte Pizza zu verwenden, wenn Sie einen mythischen Präzisions-Pizzaschneider zur Hand haben. :-)


Die meisten erfahrenen Programmierer kennen natürlich die wahre Antwort: Es gibt keine Möglichkeit, mit diesen Scheiben ein genaues Zehntel oder Fünftel der Pizza zusammenzusetzen, egal wie fein Sie sie schneiden. Sie können eine ziemlich gute Annäherung machen, und wenn Sie die Annäherung von 0,1 mit der Annäherung von 0,2 addieren, erhalten Sie eine ziemlich gute Annäherung von 0,3, aber es ist immer noch genau das, eine Annäherung.

Bei Zahlen mit doppelter Genauigkeit (dh der Genauigkeit, mit der Sie Ihre Pizza 53-mal halbieren können) sind die Zahlen, die sofort kleiner und größer als 0,1 sind, 0,09999999999999999167332731531132594682276248931884765625 und 0,1000000000000000055511151231257827021181583404541015. Letzteres liegt etwas näher an 0,1 als Ersteres, so dass ein numerischer Parser bei einer Eingabe von 0,1 Letzteres bevorzugt.

(Der Unterschied zwischen diesen beiden Zahlen ist die "kleinste Schicht", die wir entweder einschließen müssen, was eine Aufwärtsverzerrung einführt, oder ausschließen, was eine Abwärtsverzerrung einführt. Der Fachbegriff für diese kleinste Schicht ist eine ulp .)

Im Fall von 0,2 sind die Zahlen alle gleich, nur um den Faktor 2 vergrößert. Auch hier bevorzugen wir den Wert, der etwas höher als 0,2 ist.

Beachten Sie, dass in beiden Fällen die Näherungen für 0,1 und 0,2 eine leichte Aufwärtsneigung aufweisen. Wenn wir genug dieser Verzerrungen hinzufügen, werden sie die Zahl immer weiter von dem entfernen, was wir wollen, und tatsächlich ist im Fall von 0,1 + 0,2 die Verzerrung hoch genug, dass die resultierende Zahl nicht mehr die nächste Zahl ist bis 0,3.

Insbesondere ist 0,1 + 0,2 wirklich 0,1000000000000000055511151231257827021181583404541015625 + 0,200000000000000011102230246251565404236316680908203125 = 0,300000000000000044998999


PS Einige Programmiersprachen bieten auch Pizzaschneider an, mit denen Scheiben in exakte Zehntel aufgeteilt werden können . Obwohl solche Pizzaschneider ungewöhnlich sind, sollten Sie sie verwenden, wenn Sie Zugang zu einem haben, wenn es wichtig ist, genau ein Zehntel oder ein Fünftel einer Scheibe zu erhalten.

(Ursprünglich auf Quora veröffentlicht.)

Chris Jester-Young
quelle
3
Beachten Sie, dass es einige Sprachen gibt, die exakte Mathematik enthalten. Ein Beispiel ist Scheme, zum Beispiel über GNU Guile. Siehe draketo.de/english/exact-math-to-the-rescue - diese halten die Mathematik als Brüche und schneiden sich am Ende nur auf.
Arne Babenhauserheide
5
@FloatingRock Tatsächlich haben nur sehr wenige gängige Programmiersprachen rationale Zahlen eingebaut. Arne ist wie ich ein Schemer, also sind dies Dinge, an denen wir verwöhnt werden.
Chris Jester-Young
5
@ArneBabenhauserheide Ich denke, es lohnt sich hinzuzufügen, dass dies nur mit rationalen Zahlen funktioniert. Wenn Sie also mit irrationalen Zahlen wie pi rechnen, müssen Sie es als Vielfaches von pi speichern. Natürlich kann jede Berechnung mit pi nicht als exakte Dezimalzahl dargestellt werden.
Aidiakapi
13
@connexo Okay. Wie würden Sie Ihren Pizzarotator auf 36 Grad programmieren? Was ist 36 Grad? (Hinweis: Wenn Sie dies genau definieren können, haben Sie auch einen Pizzaschneider mit Scheiben und genauem Zehntel.) Mit anderen Worten, Sie können nicht 1/360 (Grad) oder 1 / haben. 10 (36 Grad) mit nur binärem Gleitkomma.
Chris Jester-Young
12
@connexo Außerdem kann "jeder Idiot" eine Pizza nicht genau um 36 Grad drehen . Menschen sind zu fehleranfällig, um etwas ganz so Genaues zu tun.
Chris Jester-Young
212

Gleitkomma-Rundungsfehler. 0,1 kann in Basis-2 aufgrund des fehlenden Primfaktors 5 nicht so genau dargestellt werden wie in Basis-10. Genauso wie 1/3 eine unendliche Anzahl von Stellen benötigt, um in Dezimalzahlen darzustellen, ist es in Basis-3 "0,1". 0.1 nimmt eine unendliche Anzahl von Ziffern in Basis-2, wo es nicht in Basis-10 ist. Und Computer haben nicht unendlich viel Speicher.

Devin Jeanpierre
quelle
133
Computer benötigen nicht unendlich viel Speicher, um 0,1 + 0,2 = 0,3 richtig zu machen
Pacerier
23
@Pacerier Sicher, sie könnten zwei Ganzzahlen mit unbegrenzter Genauigkeit verwenden, um einen Bruch darzustellen, oder sie könnten Anführungszeichen verwenden. Es ist der spezifische Begriff "binär" oder "dezimal", der dies unmöglich macht - die Idee, dass Sie eine Folge von binären / dezimalen Ziffern und irgendwo dort einen Radixpunkt haben. Um präzise rationale Ergebnisse zu erzielen, benötigen wir ein besseres Format.
Devin Jeanpierre
15
@Pacerier: Weder binäres noch dezimales Gleitkomma können 1/3 oder 1/13 genau speichern. Dezimale Gleitkommatypen können Werte der Form M / 10 ^ E präzise darstellen , sind jedoch bei der Darstellung der meisten anderen Brüche weniger genau als binäre Gleitkommazahlen ähnlicher Größe . In vielen Anwendungen ist es sinnvoller, bei beliebigen Brüchen eine höhere Präzision zu erzielen, als bei einigen "speziellen" Brüchen eine perfekte Präzision.
Supercat
13
@Pacerier Sie tun dies, wenn sie die Zahlen als binäre Floats speichern, was der Punkt der Antwort war.
Mark Amery
3
@chux: Der Unterschied in der Genauigkeit zwischen Binär- und Dezimaltypen ist nicht groß, aber der 10: 1-Unterschied in der Best- und Worst-Case-Genauigkeit für Dezimaltypen ist weitaus größer als der 2: 1-Unterschied bei Binärtypen. Ich bin gespannt, ob jemand Hardware oder geschriebene Software entwickelt hat, um effizient mit einem der Dezimaltypen zu arbeiten, da weder eine effiziente Implementierung in Hardware noch in Software möglich erscheint.
Supercat
121

Zusätzlich zu den anderen richtigen Antworten sollten Sie möglicherweise eine Skalierung Ihrer Werte in Betracht ziehen, um Probleme mit der Gleitkomma-Arithmetik zu vermeiden.

Zum Beispiel:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... Anstatt von:

var result = 0.1 + 0.2;     // result === 0.3 returns false

Die Expressions 0.1 + 0.2 === 0.3kehrt falsein JavaScript, aber glücklicherweise Integer - Arithmetik in Gleitkommazahlen exakt ist, so Dezimaldarstellung Fehler können durch Skalierung vermieden werden.

Um Gleitkommaprobleme zu vermeiden, bei denen Genauigkeit an erster Stelle steht, wird empfohlen, 1 als Ganzzahl zu behandeln, die die Anzahl der Cent darstellt: 2550Cent anstelle von 25.50Dollar.


1 Douglas Crockford: JavaScript: Die guten Teile : Anhang A - Schreckliche Teile (Seite 105) .

Daniel Vassallo
quelle
3
Das Problem ist, dass die Konvertierung selbst ungenau ist. 16,08 * 100 = 1607,9999999999998. Müssen wir die Zahl aufteilen und separat konvertieren (wie in 16 * 100 + 08 = 1608)?
Jason
38
Die Lösung besteht darin, alle Ihre Berechnungen in Ganzzahlen durchzuführen und dann durch Ihren Anteil (in diesem Fall 100) zu dividieren und nur bei Darstellung der Daten zu runden. Dadurch wird sichergestellt, dass Ihre Berechnungen immer präzise sind.
David Granado
15
Nur um ein wenig zu picken: Ganzzahlige Arithmetik ist nur im Gleitkomma bis zu einem Punkt genau (Wortspiel beabsichtigt). Wenn die Zahl größer als 0x1p53 ist (um die hexadezimale Gleitkomma-Notation von Java 7 zu verwenden, = 9007199254740992), ist die ulp an diesem Punkt 2 und daher wird 0x1p53 + 1 auf 0x1p53 abgerundet (und 0x1p53 + 3 wird auf 0x1p53 + aufgerundet 4, wegen Round-to-Even). :-D Aber wenn Ihre Zahl kleiner als 9 Billiarden ist, sollte es Ihnen gut gehen. :-P
Chris Jester-Young
2
Jason, du solltest nur das Ergebnis runden (int) (16.08 * 100 + 0.5)
Mikhail Semenov
@CodyBugstein " Wie können Sie .1 + .2 dazu bringen, .3 anzuzeigen ? " Schreiben Sie eine benutzerdefinierte Druckfunktion, um die Dezimalstelle an der gewünschten Stelle zu platzieren.
RonJohn
113

Meine Antwort ist ziemlich lang, deshalb habe ich sie in drei Abschnitte unterteilt. Da es sich bei der Frage um Gleitkomma-Mathematik handelt, habe ich den Schwerpunkt darauf gelegt, was die Maschine tatsächlich tut. Ich habe es auch spezifisch für die doppelte Genauigkeit (64 Bit) gemacht, aber das Argument gilt gleichermaßen für jede Gleitkomma-Arithmetik.

Präambel

Eine IEEE 754- Nummer mit binärem Gleitkommaformat (binär 64) mit doppelter Genauigkeit repräsentiert eine Nummer des Formulars

Wert = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

in 64 Bit:

  • Das erste Bit ist das Vorzeichenbit : 1Wenn die Zahl negativ ist, 0andernfalls 1 .
  • Die nächsten 11 Bits sind der Exponent , der um 1023 versetzt ist. Mit anderen Worten, nach dem Lesen der Exponentenbits von einer Zahl mit doppelter Genauigkeit muss 1023 subtrahiert werden, um die Zweierpotenz zu erhalten.
  • Die verbleibenden 52 Bits sind der Signifikant (oder die Mantisse). In der Mantisse wird ein "implizites" 1.immer 2 weggelassen, da das höchstwertige Bit eines Binärwerts ist 1.

1 - IEEE 754 ermöglicht das Konzept einer vorzeichenbehafteten Null - +0und -0wird unterschiedlich behandelt: 1 / (+0)ist positive Unendlichkeit; 1 / (-0)ist negative Unendlichkeit. Bei Nullwerten sind die Mantissen- und Exponentenbits alle Null. Hinweis: Nullwerte (+0 und -0) werden explizit nicht als denormal 2 klassifiziert .

2 - Dies ist nicht der Fall für denormale Zahlen , die einen Offset-Exponenten von Null (und einen implizierten 0.) haben. Der Bereich der denormalen Zahlen mit doppelter Genauigkeit ist d min ≤ | x | ≤ d max , wobei d min (die kleinste darstellbare Zahl ungleich Null) 2 - 1023 - 51 (≈ 4,94 * 10 - 324 ) und d max (die größte denormale Zahl, für die die Mantisse vollständig aus 1s besteht) 2 - 1023 beträgt + 1 - 2 - 1023 - 51 (≈ 2,225 * 10 - 308 ).


Eine Zahl mit doppelter Genauigkeit in binär umwandeln

Es gibt viele Online-Konverter, die eine Gleitkommazahl mit doppelter Genauigkeit in eine Binärzahl konvertieren (z. B. bei binaryconvert.com ). Hier ist jedoch ein Beispiel für einen C # -Code, um die IEEE 754-Darstellung für eine Zahl mit doppelter Genauigkeit zu erhalten (ich trenne die drei Teile mit Doppelpunkten ( :) ::

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Auf den Punkt gebracht: die ursprüngliche Frage

(Für die TL; DR-Version nach unten springen)

Cato Johnston (der Fragesteller) fragte, warum 0,1 + 0,2! = 0,3.

Die IEEE 754-Darstellungen der Werte sind binär geschrieben (mit Doppelpunkten, die die drei Teile trennen):

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Beachten Sie, dass die Mantisse aus wiederkehrenden Ziffern von besteht 0011. Dies ist der Schlüssel , warum die Berechnungen fehlerhaft sind - 0,1, 0,2 und 0,3 können nicht genau in einer endlichen Anzahl von Binärbits binär dargestellt werden , und es können nicht mehr als 1/9, 1/3 oder 1/7 genau in dargestellt werden Dezimalstellen .

Beachten Sie auch, dass wir die Potenz im Exponenten um 52 verringern und den Punkt in der binären Darstellung um 52 Stellen nach rechts verschieben können (ähnlich wie 10 -3 * 1,23 == 10 -5 * 123). Dies ermöglicht es uns dann, die binäre Darstellung als den genauen Wert darzustellen, den sie in der Form a * 2 p darstellt . Dabei ist 'a' eine ganze Zahl.

Wenn Sie die Exponenten in Dezimalzahlen konvertieren, den Versatz entfernen und die implizierten Exponenten 1(in eckigen Klammern) erneut hinzufügen , sind 0,1 und 0,2:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

Um zwei Zahlen hinzuzufügen, muss der Exponent derselbe sein, dh:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

Da die Summe nicht die Form 2 n * 1 hat. {Bbb} erhöhen wir den Exponenten um eins und verschieben den Dezimalpunkt ( binär ), um Folgendes zu erhalten:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

Die Mantisse enthält jetzt 53 Bits (das 53. Bit steht in der obigen Zeile in eckigen Klammern). Der Standardrundungsmodus für IEEE 754 ist ‚ Round zu Nächsten ‘ - das heißt , wenn eine Zahl x zwischen zwei Werten a und b , in denen der Wert der niedrigstwertigen Bits Null gewählt wird .

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

Beachten Sie, dass sich a und b nur im letzten Bit unterscheiden. ...0011+ 1= ...0100. In diesem Fall ist der Wert mit dem niedrigstwertigen Bit Null b , daher lautet die Summe:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

während die binäre Darstellung von 0,3 ist:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

was sich nur von der binären Darstellung der Summe von 0,1 und 0,2 um 2 -54 unterscheidet .

Die binäre Darstellung von 0,1 und 0,2 ist die genaueste Darstellung der nach IEEE 754 zulässigen Zahlen. Die Hinzufügung dieser Darstellung aufgrund des Standardrundungsmodus führt zu einem Wert, der sich nur im niedrigstwertigen Bit unterscheidet.

TL; DR

Schreiben Sie 0.1 + 0.2in eine IEEE 754-Binärdarstellung (mit Doppelpunkten, die die drei Teile trennen) und vergleichen Sie sie mit 0.3(ich habe die verschiedenen Bits in eckige Klammern gesetzt):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Zurück in Dezimalzahlen konvertiert, sind diese Werte:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

Der Unterschied beträgt genau 2 -54 , was ~ 5,5511151231258 × 10 -17 ist - im Vergleich zu den ursprünglichen Werten (für viele Anwendungen) unbedeutend.

Der Vergleich der letzten Bits einer Gleitkommazahl ist von Natur aus gefährlich, wie jeder weiß , der das berühmte " Was jeder Informatiker über Gleitkomma-Arithmetik wissen sollte " (das alle wichtigen Teile dieser Antwort abdeckt) liest .

Die meisten Taschenrechner verwenden zusätzliche Schutzziffern , um dieses Problem zu umgehen. Dies 0.1 + 0.2würde sich ergeben 0.3: Die letzten paar Bits sind gerundet.

Wai Ha Lee
quelle
14
Meine Antwort wurde kurz nach dem Posten abgelehnt. Ich habe seitdem viele Änderungen vorgenommen (einschließlich des expliziten Hinweises auf die wiederkehrenden Bits beim Schreiben von 0.1 und 0.2 in Binärform, die ich im Original weggelassen hatte). Könnten Sie mir bitte ein Feedback geben, damit ich meine Antwort verbessern kann, wenn der Abwähler dies nicht sieht? Ich habe das Gefühl, dass meine Antwort etwas Neues hinzufügt, da die Behandlung der Summe in IEEE 754 in anderen Antworten nicht auf die gleiche Weise behandelt wird. Während "Was jeder Informatiker wissen sollte ..." dasselbe Material behandelt, befasst sich meine Antwort speziell mit dem Fall von 0,1 + 0,2.
Wai Ha Lee
57

Im Computer gespeicherte Gleitkommazahlen bestehen aus zwei Teilen, einer Ganzzahl und einem Exponenten, zu dem die Basis genommen und mit dem Ganzzahlteil multipliziert wird.

Wenn der Computer in Basis 10 arbeiten 0.1würde 1 x 10⁻¹, 0.2wäre , wäre 2 x 10⁻¹und 0.3wäre 3 x 10⁻¹. Ganzzahlige Mathematik ist einfach und genau, daher führt das Hinzufügen 0.1 + 0.2offensichtlich zu 0.3.

Computer arbeiten normalerweise nicht in Basis 10, sondern in Basis 2. Sie können immer noch genaue Ergebnisse für einige Werte erhalten, z. B. 0.5ist 1 x 2⁻¹und 0.25ist 1 x 2⁻², und das Hinzufügen dieser Ergebnisse in 3 x 2⁻²oder 0.75. Genau.

Das Problem tritt bei Zahlen auf, die genau in Basis 10, aber nicht in Basis 2 dargestellt werden können. Diese Zahlen müssen auf das nächste Äquivalent gerundet werden. Angenommen , die sehr häufig IEEE 64-Bit - Gleitkomma - Format, ist die nächste Nummer 0.1ist 3602879701896397 x 2⁻⁵⁵, und die nächste Nummer 0.2ist 7205759403792794 x 2⁻⁵⁵; Das Addieren ergibt 10808639105689191 x 2⁻⁵⁵oder einen exakten Dezimalwert von 0.3000000000000000444089209850062616169452667236328125. Gleitkommazahlen werden in der Regel zur Anzeige gerundet.

Mark Ransom
quelle
2
@Mark Vielen Dank für diese klare Erklärung, aber dann stellt sich die Frage, warum 0,1 + 0,4 genau 0,5 ergibt (zumindest in Python 3). Was ist auch der beste Weg, um die Gleichheit bei der Verwendung von Floats in Python 3 zu überprüfen?
Pchegoor
2
@ user2417881 IEEE-Gleitkommaoperationen haben Rundungsregeln für jede Operation, und manchmal kann die Rundung eine genaue Antwort liefern, selbst wenn die beiden Zahlen ein wenig abweichen. Die Details sind zu lang für einen Kommentar und ich bin sowieso kein Experte darin. Wie Sie in dieser Antwort sehen, ist 0,5 eine der wenigen Dezimalstellen, die binär dargestellt werden können, aber das ist nur ein Zufall. Informationen zur Gleichheitsprüfung finden Sie unter stackoverflow.com/questions/5595425/… .
Mark Ransom
1
@ user2417881 Ihre Frage hat mich fasziniert, also habe ich daraus eine vollständige Frage und Antwort gemacht: stackoverflow.com/q/48374522/5987
Mark Ransom
47

Gleitkomma-Rundungsfehler. Von dem, was jeder Informatiker über Gleitkomma-Arithmetik wissen sollte :

Das unendliche Zusammendrücken vieler reeller Zahlen in eine endliche Anzahl von Bits erfordert eine ungefähre Darstellung. Obwohl es unendlich viele Ganzzahlen gibt, kann das Ergebnis von Ganzzahlberechnungen in den meisten Programmen in 32 Bit gespeichert werden. Im Gegensatz dazu ergeben die meisten Berechnungen mit reellen Zahlen bei einer festgelegten Anzahl von Bits Größen, die mit so vielen Bits nicht genau dargestellt werden können. Daher muss das Ergebnis einer Gleitkommaberechnung häufig gerundet werden, um wieder in seine endliche Darstellung zu passen. Dieser Rundungsfehler ist das charakteristische Merkmal der Gleitkommaberechnung.

Brett Daniel
quelle
33

Meine Problemumgehung:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

Die Genauigkeit bezieht sich auf die Anzahl der Stellen, die während des Hinzufügens nach dem Dezimalpunkt beibehalten werden sollen.

Justineo
quelle
30

Es wurden viele gute Antworten veröffentlicht, aber ich möchte noch eine anhängen.

Nicht alle Zahlen können über Floats / Doubles dargestellt werden . Beispielsweise wird die Zahl "0.2" im Gleitkomma-Standard IEEE754 mit einfacher Genauigkeit als "0.200000003" dargestellt.

Das Modell zum Speichern von reellen Zahlen unter der Haube repräsentiert Float-Zahlen als

Geben Sie hier die Bildbeschreibung ein

Auch wenn Sie 0.2leicht tippen können FLT_RADIXund DBL_RADIX2 ist; nicht 10 für einen Computer mit FPU, der "IEEE-Standard für binäre Gleitkomma-Arithmetik (ISO / IEEE Std 754-1985)" verwendet.

Es ist also etwas schwierig, solche Zahlen genau darzustellen. Auch wenn Sie diese Variable explizit ohne Zwischenberechnung angeben.

Bruziuz
quelle
28

Einige Statistiken bezogen sich auf diese berühmte Frage mit doppelter Genauigkeit.

Wenn wir alle Werte ( a + b ) in einem Schritt von 0,1 (von 0,1 bis 100) addieren, haben wir eine Wahrscheinlichkeit von ~ 15% für Präzisionsfehler . Beachten Sie, dass der Fehler zu etwas größeren oder kleineren Werten führen kann. Hier sind einige Beispiele:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

Wenn alle Werte ( a - b, wobei a> b ) mit einem Schritt von 0,1 (von 100 bis 0,1) subtrahiert werden, haben wir eine Wahrscheinlichkeit von ~ 34% für Präzisionsfehler . Hier sind einige Beispiele:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% und 34% sind in der Tat sehr groß. Verwenden Sie BigDecimal daher immer dann, wenn Präzision von großer Bedeutung ist. Mit 2 Dezimalstellen (Schritt 0,01) verschlechtert sich die Situation etwas mehr (18% und 36%).

Kostas Chalkias
quelle
28

Nein, nicht gebrochen, aber die meisten Dezimalbrüche müssen angenähert werden

Zusammenfassung

Gleitkomma-Arithmetik ist genau, leider passt sie nicht gut zu unserer üblichen Basis-10-Zahlendarstellung. Es stellt sich also heraus, dass wir häufig Eingaben geben, die etwas von dem abweichen, was wir geschrieben haben.

Selbst einfache Zahlen wie 0,01, 0,02, 0,03, 0,04 ... 0,24 können nicht genau als binäre Brüche dargestellt werden. Wenn Sie 0,01, 0,02, 0,03 ... hochzählen, erhalten Sie erst bei 0,25 den ersten in Basis 2 darstellbaren Bruch . Wenn Sie dies mit FP versucht hätten, wäre Ihre 0,01 leicht gesunken, sodass die einzige Möglichkeit, 25 davon auf eine schöne exakte 0,25 zu addieren, eine lange Kausalkette mit Schutzbits und Rundungen erforderlich gemacht hätte. Es ist schwer vorherzusagen, also werfen wir unsere Hände hoch und sagen "FP ist ungenau", aber das stimmt nicht wirklich.

Wir geben der FP-Hardware ständig etwas, das in Basis 10 einfach erscheint, in Basis 2 jedoch ein sich wiederholender Bruchteil ist.

Wie ist es passiert?

Wenn wir in Dezimalzahl schreiben, ist jeder Bruch (insbesondere jede abschließende Dezimalstelle) eine rationale Zahl der Form

           a / (2 n x 5 m )

In binär erhalten wir nur den 2 n- Term, das heißt:

           a / 2 n

Also in dezimal, können wir nicht repräsentieren 1 / 3 . Da 10 der Basis 2 als Hauptfaktor enthält, jede Zahl , die wir als eine Binärbruch schreiben auch sein kann als Basis 10 Fraktion geschrieben. Allerdings ist kaum etwas, was wir als Basis- 10- Bruch schreiben, binär darstellbar. Im Bereich von 0,01, 0,02, 0,03 ... 0,99 können in unserem FP-Format nur drei Zahlen dargestellt werden: 0,25, 0,50 und 0,75, da es sich um 1/4, 1/2 und 3/4 handelt, alle Zahlen mit einem Primfaktor, der nur den 2 n- Term verwendet.

In Basis 10 können wir repräsentieren nicht 1 / 3 . Aber im Binär-, können wir nicht 1 / 10 oder 1 / 3 .

Während also jeder binäre Bruch dezimal geschrieben werden kann, ist das Gegenteil nicht der Fall. Tatsächlich wiederholen sich die meisten Dezimalbrüche binär.

Damit klarkommen

Entwickler werden normalerweise angewiesen, <epsilon- Vergleiche durchzuführen. Besser wäre es, auf ganzzahlige Werte zu runden (in der C-Bibliothek: round () und roundf (), dh im FP-Format zu bleiben) und dann zu vergleichen. Das Runden auf eine bestimmte Dezimalbruchlänge löst die meisten Probleme mit der Ausgabe.

Bei realen Problemen mit der Zahlenkalkulation (die Probleme, für die FP auf frühen, furchtbar teuren Computern erfunden wurde) sind die physikalischen Konstanten des Universums und alle anderen Messungen nur einer relativ kleinen Anzahl signifikanter Zahlen bekannt, also dem gesamten Problemraum war sowieso "ungenau". FP "Genauigkeit" ist bei dieser Art von Anwendung kein Problem.

Das ganze Problem entsteht wirklich, wenn Leute versuchen, FP für das Bohnenzählen zu verwenden. Es funktioniert dafür, aber nur, wenn Sie sich an ganzzahlige Werte halten, was den Sinn der Verwendung zunichte macht. Aus diesem Grund haben wir all diese Softwarebibliotheken für Dezimalbrüche.

Ich liebe die Pizza-Antwort von Chris , weil sie das eigentliche Problem beschreibt, nicht nur das übliche Handwinken über "Ungenauigkeit". Wenn FP einfach "ungenau" wäre, könnten wir das beheben und hätten es vor Jahrzehnten getan. Der Grund, warum wir das nicht getan haben, ist, dass das FP-Format kompakt und schnell ist und der beste Weg ist, viele Zahlen zu knacken. Es ist auch ein Erbe aus dem Weltraumzeitalter und dem Wettrüsten und frühen Versuchen, große Probleme mit sehr langsamen Computern mit kleinen Speichersystemen zu lösen. (Manchmal einzelne Magnetkerne für 1-Bit-Speicher, aber das ist eine andere Geschichte. )

Fazit

Wenn Sie nur Bohnen in einer Bank zählen, funktionieren Softwarelösungen, die in erster Linie Dezimalzeichenfolgen verwenden, einwandfrei. Aber so kann man keine Quantenchromodynamik oder Aerodynamik machen.

DigitalRoss
quelle
Das Runden auf die nächste Ganzzahl ist nicht in allen Fällen ein sicherer Weg, um das Vergleichsproblem zu lösen. 0,4999998 und 0,500001 runden auf verschiedene ganze Zahlen, sodass um jeden Rundungsschnittpunkt eine "Gefahrenzone" vorhanden ist. (Ich weiß, dass diese Dezimalzeichenfolgen wahrscheinlich nicht genau als IEEE-Binär-Floats dargestellt werden können.)
Peter Cordes
1
Auch wenn Gleitkomma ein "Legacy" -Format ist, ist es sehr gut gestaltet. Ich weiß nichts, was irgendjemand ändern würde, wenn er es jetzt neu gestalten würde. Je mehr ich darüber lerne, desto mehr denke ich, dass es wirklich gut gestaltet ist. Beispiel: Der voreingenommene Exponent bedeutet, dass aufeinanderfolgende binäre Floats aufeinanderfolgende ganzzahlige Darstellungen haben, sodass Sie nextafter()die binäre Darstellung eines IEEE- Floats mit einem ganzzahligen Inkrement oder Dekrement implementieren können . Sie können Floats auch als Ganzzahlen vergleichen und die richtige Antwort erhalten, außer wenn beide negativ sind (aufgrund der Vorzeichengröße gegenüber dem 2er-Komplement).
Peter Cordes
Ich bin nicht einverstanden, die Floats sollten als Dezimalstellen und nicht als Binärdateien gespeichert werden und alle Probleme sind gelöst.
Ronen Festinger
Sollte " x / (2 ^ n + 5 ^ n) " nicht " x / (2 ^ n * 5 ^ n) " sein?
Wai Ha Lee
@RonenFestinger - was ist mit 1/3?
Stephen C
19

Haben Sie die Klebebandlösung ausprobiert?

Versuchen Sie festzustellen, wann Fehler auftreten, und beheben Sie sie mit kurzen if-Anweisungen. Es ist nicht schön, aber für einige Probleme ist es die einzige Lösung, und dies ist eine davon.

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

Ich hatte das gleiche Problem in einem wissenschaftlichen Simulationsprojekt in c #, und ich kann Ihnen sagen, dass wenn Sie den Schmetterlingseffekt ignorieren, es sich in einen großen fetten Drachen verwandeln und Sie in den a ** beißen wird

Workoverflow
quelle
19

Um die beste Lösung anzubieten, kann ich sagen, dass ich folgende Methode entdeckt habe:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Lassen Sie mich erklären, warum es die beste Lösung ist. Wie in den obigen Antworten erwähnt, ist es eine gute Idee, die gebrauchsfertige Javascript toFixed () -Funktion zu verwenden, um das Problem zu lösen. Aber höchstwahrscheinlich werden Sie auf einige Probleme stoßen.

Stellen Sie sich vor, Sie addieren zwei Float-Zahlen wie 0.2und 0.7hier ist es : 0.2 + 0.7 = 0.8999999999999999.

Ihr erwartetes Ergebnis war 0.9, dass Sie in diesem Fall ein Ergebnis mit 1-stelliger Genauigkeit benötigen. Sie hätten also verwenden sollen, (0.2 + 0.7).tofixed(1) aber Sie können toFixed () nicht einfach einen bestimmten Parameter geben, da dieser beispielsweise von der angegebenen Nummer abhängt

`0.22 + 0.7 = 0.9199999999999999`

In diesem Beispiel benötigen Sie eine Genauigkeit von 2 Stellen, damit dies so ist toFixed(2) , also sollte es der Parameter sein, der zu jeder gegebenen Gleitkommazahl passt?

Man könnte sagen, dann sei es in jeder Situation 10:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

Verdammt! Was machst du mit diesen unerwünschten Nullen nach 9? Es ist an der Zeit, es in Float umzuwandeln, damit es Ihren Wünschen entspricht:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Nachdem Sie die Lösung gefunden haben, ist es besser, sie wie folgt anzubieten:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

Probieren wir es selbst aus:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Sie können es folgendermaßen verwenden:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Da W3SCHOOLS vorschlägt, dass es auch eine andere Lösung gibt, können Sie multiplizieren und dividieren, um das obige Problem zu lösen:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

Denken Sie daran, dass (0.2 + 0.1) * 10 / 10das überhaupt nicht funktioniert, obwohl es das gleiche scheint! Ich bevorzuge die erste Lösung, da ich sie als eine Funktion anwenden kann, die den Eingangs-Float in einen genauen Ausgangs-Float umwandelt.

Mohammad Musavi
quelle
Das machte mir echte Kopfschmerzen. Ich summiere 12 Float-Zahlen, dann zeige ich die Summe und den Durchschnitt, wenn diese Zahlen. Die Verwendung von toFixed () kann die Summierung von 2 Zahlen korrigieren, aber wenn mehrere Zahlen summiert werden, ist der Sprung signifikant.
Nuryagdy Mustapayev
@Nuryagdy Mustapayev Ich habe Ihre Absicht nicht verstanden, da ich getestet habe, bevor Sie 12 Gleitkommazahlen summieren können, dann die Funktion floatify () für das Ergebnis verwenden und dann tun, was Sie wollen. Ich habe kein Problem damit festgestellt.
Mohammad Musavi
Ich sage nur in meiner Situation, in der ich ungefähr 20 Parameter und 20 Formeln habe, in denen das Ergebnis jeder Formel von anderen abhängt, hat diese Lösung nicht geholfen.
Nuryagdy Mustapayev
16

Diese seltsamen Zahlen erscheinen, weil Computer zu Berechnungszwecken ein binäres Zahlensystem (Basis 2) verwenden, während wir Dezimalzahlen (Basis 10) verwenden.

Es gibt eine Mehrheit von Bruchzahlen, die weder binär noch dezimal oder beides genau dargestellt werden können. Ergebnis - Es ergibt sich eine aufgerundete (aber genaue) Zahl.

Piyush S528
quelle
Ich verstehe Ihren zweiten Absatz überhaupt nicht.
Nae
1
@Nae Ich würde den zweiten Absatz als "Die Mehrheit der Brüche kann weder dezimal noch binär exakt dargestellt werden . Daher werden die meisten Ergebnisse abgerundet - obwohl sie immer noch genau auf die Anzahl der Bits / Ziffern abgestimmt sind, die der Darstellung inhärent sind." verwendet werden."
Steve Summit
15

Viele der zahlreichen Duplikate dieser Frage fragen nach den Auswirkungen der Gleitkomma-Rundung auf bestimmte Zahlen. In der Praxis ist es einfacher, ein Gefühl dafür zu bekommen, wie es funktioniert, wenn man sich die genauen Ergebnisse von Berechnungen von Interesse ansieht, als nur darüber zu lesen. Einige Sprachen bieten Möglichkeiten dazu - beispielsweise das Konvertieren von a floatoder doubleinBigDecimal in Java.

Da es sich um eine sprachunabhängige Frage handelt, sind sprachunabhängige Tools erforderlich, z. B. ein Konverter von Dezimal zu Gleitkomma .

Anwenden auf die Zahlen in der Frage, die als Doppel behandelt werden:

0.1 konvertiert zu 0.1000000000000000055511151231257827021181583404541015625,

0,2 konvertiert zu 0,200000000000000011102230246251565404236316680908203125,

0,3 konvertiert zu 0,299999999999999988897769753748434595763683319091796875 und

0.30000000000000004 wird in 0.3000000000000000444089209850062616169452667236328125 konvertiert.

Hinzufügen der ersten beiden Zahlen manuell oder in einem Dezimalrechner wie dem Full Precision Calculator hinzufügen, wird die genaue Summe der tatsächlichen Eingaben mit 0,3000000000000000166533453693773481063544750213623046875 angezeigt.

Wenn es auf das Äquivalent von 0,3 abgerundet würde, wäre der Rundungsfehler 0,0000000000000000277555756156289135105907917022705078125. Das Aufrunden auf das Äquivalent von 0,30000000000000004 ergibt ebenfalls einen Rundungsfehler von 0,0000000000000000277555756156289135105907917022705078125. Es gilt der runde bis gleichmäßige Krawattenbrecher.

Zurück zum Gleitkommakonverter: Die rohe Hexadezimalzahl für 0,30000000000000004 lautet 3fd3333333333334, was mit einer geraden Ziffer endet und daher das richtige Ergebnis ist.

Patricia Shanahan
quelle
2
An die Person, deren Bearbeitung ich gerade zurückgesetzt habe: Ich halte Code-Anführungszeichen für geeignet, um Code zu zitieren. Diese sprachneutrale Antwort enthält überhaupt keinen zitierten Code. Zahlen können in englischen Sätzen verwendet werden und werden dadurch nicht in Code umgewandelt.
Patricia Shanahan
Dies ist wahrscheinlich der Grund, warum jemand Ihre Zahlen als Code formatiert hat - nicht zur Formatierung, sondern zur besseren Lesbarkeit.
Wai Ha Lee
... bezieht sich die Runde auf sogar auf die binäre Darstellung, nicht auf die Dezimaldarstellung . Sehen Sie dies oder zum Beispiel dies .
Wai Ha Lee
@WaiHaLee Ich habe den ungeraden / geraden Test nicht auf Dezimalzahlen angewendet, sondern nur auf Hexadezimalzahlen. Eine hexadezimale Ziffer ist gerade dann und nur dann gleich, wenn das niedrigstwertige Bit ihrer binären Erweiterung Null ist.
Patricia Shanahan
14

Da niemand dies erwähnt hat ...

Einige Hochsprachen wie Python und Java verfügen über Tools zur Überwindung von Einschränkungen durch binäre Gleitkommazahlen. Zum Beispiel:

  • Pythons decimalModul und Javas BigDecimalKlasse , die Zahlen intern mit Dezimalschreibweise darstellen (im Gegensatz zur Binärschreibweise). Beide haben eine begrenzte Genauigkeit, sind also immer noch fehleranfällig, lösen jedoch die häufigsten Probleme mit der binären Gleitkomma-Arithmetik.

    Dezimalstellen sind im Umgang mit Geld sehr schön: Zehn Cent plus zwanzig Cent sind immer genau dreißig Cent:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    Das Python- decimalModul basiert auf dem IEEE-Standard 854-1987 .

  • Pythons fractionsModul und Apache Common BigFractionKlasse . Beide stellen rationale Zahlen als (numerator, denominator)Paare dar und können genauere Ergebnisse liefern als dezimale Gleitkomma-Arithmetik.

Keine dieser Lösungen ist perfekt (insbesondere wenn wir uns die Leistungen ansehen oder eine sehr hohe Präzision benötigen), aber sie lösen dennoch eine große Anzahl von Problemen mit der binären Gleitkomma-Arithmetik.

Andrea Corbellini
quelle
14

Kann ich nur hinzufügen; Die Leute gehen immer davon aus, dass dies ein Computerproblem ist, aber wenn Sie mit Ihren Händen zählen (Basis 10), können Sie es nur bekommen, (1/3+1/3=2/3)=truewenn Sie unendlich sind, um 0,333 ... zu 0,333 ... hinzuzufügen, genau wie bei dem (1/10+2/10)!==3/10Problem in der Basis 2, Sie kürzen es auf 0,333 + 0,333 = 0,666 und runden es wahrscheinlich auf 0,667, was auch technisch ungenau wäre.

Zählen Sie ternär, und Drittel sind jedoch kein Problem - vielleicht würde ein Rennen mit 15 Fingern an jeder Hand fragen, warum Ihre Dezimalrechnung gebrochen wurde ...


quelle
Da Menschen Dezimalzahlen verwenden, sehe ich keinen guten Grund, warum die Gleitkommazahlen standardmäßig nicht als Dezimalzahlen dargestellt werden, sodass wir genaue Ergebnisse erhalten.
Ronen Festinger
Menschen verwenden viele andere Basen als Basis 10 (Dezimalstellen), wobei Binär diejenige ist, die wir am häufigsten für die Berechnung verwenden. Der 'gute Grund' ist, dass Sie einfach nicht jeden Bruch in jeder Basis darstellen können.
Die binäre Arithmetik von @RonenFestinger ist auf Computern einfach zu implementieren, da nur acht grundlegende Operationen mit Ziffern erforderlich sind: Sagen Sie $ a $, $ b $ in $ 0,1 $. Alles, was Sie wissen müssen, ist $ \ operatorname {xor} (a, b). $ und $ \ operatorname {cb} (a, b) $, wobei xor exklusiv ist oder und cb das "Übertragsbit" ist, das in allen Fällen $ 0 $ ist, außer wenn $ a = 1 = b $, in diesem Fall haben wir eine (in der Tat spart die Kommutativität aller Operationen 2 $ Fälle und alles, was Sie brauchen, sind 6 $ Regeln). Für die Dezimalerweiterung müssen $ 10 \ mal 11 $ (in Dezimalschreibweise) Fälle gespeichert werden und $ 10 $ unterschiedliche Zustände für jedes Bit und Verschwendung von Speicher auf dem Übertrag.
Oskar Limka
@RonenFestinger - Dezimal ist NICHT genauer. Das sagt diese Antwort. Für jede Basis, die Sie ausgewählt haben, gibt es rationale Zahlen (Brüche), die sich unendlich wiederholende Ziffernfolgen ergeben. Für die Aufzeichnung einige der ersten Computer haben die Verwendung der Basis 10 Darstellungen für Zahlen, aber der Pionier Computer - Hardware - Designer bald festgestellt , dass Basis 2 war viel einfacher und effizienter zu implementieren.
Stephen C
9

Die Art der Gleitkomma-Mathematik, die in einem digitalen Computer implementiert werden kann, verwendet notwendigerweise eine Annäherung der reellen Zahlen und Operationen auf ihnen. (Die Standardversion umfasst mehr als fünfzig Seiten Dokumentation und verfügt über ein Komitee, das sich mit den Errata und der weiteren Verfeinerung befasst.)

Diese Annäherung ist eine Mischung aus Näherungen verschiedener Art, von denen jede aufgrund ihrer spezifischen Art der Abweichung von der Genauigkeit entweder ignoriert oder sorgfältig berücksichtigt werden kann. Es gibt auch eine Reihe expliziter Ausnahmefälle sowohl auf Hardware- als auch auf Softwareebene, an denen die meisten Menschen vorbeigehen und so tun, als würden sie es nicht bemerken.

Wenn Sie eine unendliche Genauigkeit benötigen (z. B. mit der Zahl π anstelle eines der vielen kürzeren Stellvertreter), sollten Sie stattdessen ein symbolisches Mathematikprogramm schreiben oder verwenden.

Aber wenn Sie mit der Idee einverstanden sind, dass Gleitkomma-Mathematik manchmal unscharf in Wert und Logik ist und sich Fehler schnell ansammeln können und Sie Ihre Anforderungen und Tests schreiben können, um dies zu berücksichtigen, kann Ihr Code häufig mit dem auskommen, was darin enthalten ist Ihre FPU.

Blair Houghton
quelle
9

Nur zum Spaß habe ich mit der Darstellung von Floats gespielt, wobei ich den Definitionen aus dem Standard C99 gefolgt bin, und den folgenden Code geschrieben.

Der Code druckt die binäre Darstellung von Floats in 3 getrennten Gruppen

SIGN EXPONENT FRACTION

und danach wird eine Summe gedruckt, die, wenn sie mit ausreichender Genauigkeit summiert wird, den Wert anzeigt, der tatsächlich in der Hardware vorhanden ist.

Also wenn du schreibst float x = 999... , transformiert der Compiler diese Zahl in eine von der Funktion gedruckte Bitdarstellung xx, sodass die von der Funktion gedruckte Summe yyder angegebenen Zahl entspricht.

In Wirklichkeit ist diese Summe nur eine Annäherung. Für die Nummer 999.999.999 fügt der Compiler in die Bitdarstellung des Floats die Nummer 1.000.000.000 ein

Nach dem Code füge ich eine Konsolensitzung hinzu, in der ich die Summe der Terme für beide Konstanten (minus PI und 999999999) berechne, die tatsächlich in der Hardware vorhanden sind und dort vom Compiler eingefügt wurden.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Hier ist eine Konsolensitzung, in der ich den tatsächlichen Wert des in der Hardware vorhandenen Floats berechne. Ich habe bcdie Summe der vom Hauptprogramm ausgegebenen Begriffe gedruckt. Man kann diese Summe auch in Python reploder ähnliches einfügen .

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Das ist es. Der Wert von 999999999 ist in der Tat

999999999.999999446351872

Sie können auch überprüfen bc, ob -3.14 ebenfalls gestört ist. Vergessen Sie nicht, einen scaleFaktor einzugebenbc .

Die angezeigte Summe ist was in der Hardware. Der Wert, den Sie durch Berechnung erhalten, hängt von der von Ihnen festgelegten Skala ab. Ich habe den scaleFaktor auf 15 gesetzt. Mathematisch gesehen scheint es mit unendlicher Genauigkeit 1.000.000.000 zu sein.

Alinsoar
quelle
5

Eine andere Sichtweise: Verwendet werden 64 Bit zur Darstellung von Zahlen. Infolgedessen können nicht mehr als 2 ** 64 = 18.446.744.073.709.551.616 verschiedene Zahlen präzise dargestellt werden.

Laut Math gibt es jedoch bereits unendlich viele Dezimalstellen zwischen 0 und 1. IEE 754 definiert eine Codierung, um diese 64 Bit effizient für einen viel größeren Zahlenraum plus NaN und +/- Unendlich zu nutzen, sodass Lücken zwischen genau dargestellten Zahlen bestehen, die mit gefüllt sind Zahlen nur angenähert.

Leider sitzt 0,3 in einer Lücke.

Torsten Becker
quelle
4

Stellen Sie sich vor, Sie arbeiten in Basis zehn mit beispielsweise 8 Stellen Genauigkeit. Sie prüfen ob

1/3 + 2 / 3 == 1

und lernen, dass dies zurückkehrt false. Warum? Nun, als reelle Zahlen haben wir

1/3 = 0,333 .... und 2/3 = 0,666 ....

Wenn wir acht Dezimalstellen abschneiden, erhalten wir

0.33333333 + 0.66666666 = 0.99999999

das unterscheidet sich natürlich 1.00000000von genau 0.00000001.


Die Situation für Binärzahlen mit einer festen Anzahl von Bits ist genau analog. Als reelle Zahlen haben wir

1/10 = 0,0001100110011001100 ... (Basis 2)

und

1/5 = 0,0011001100110011001 ... (Basis 2)

Wenn wir diese beispielsweise auf sieben Bits kürzen würden, würden wir bekommen

0.0001100 + 0.0011001 = 0.0100101

während auf der anderen Seite,

3/10 = 0,01001100110011 ... (Basis 2)

was auf sieben Bits abgeschnitten ist 0.0100110, und diese unterscheiden sich um genau 0.0000001.


Die genaue Situation ist etwas subtiler, da diese Zahlen normalerweise in wissenschaftlicher Notation gespeichert sind. Also zum Beispiel, anstatt 1/10 zu speichern, wie 0.0001100wir es als etwas speichern können 1.10011 * 2^-4, abhängig davon, wie viele Bits wir für den Exponenten und die Mantisse zugewiesen haben. Dies wirkt sich darauf aus, wie viele Stellen Genauigkeit Sie für Ihre Berechnungen erhalten.

Das Ergebnis ist, dass Sie aufgrund dieser Rundungsfehler im Wesentlichen niemals == für Gleitkommazahlen verwenden möchten. Stattdessen können Sie überprüfen, ob der absolute Wert ihrer Differenz kleiner als eine feste kleine Zahl ist.

Daniel McLaury
quelle
4

Seit Python 3.5 können Sie die math.isclose()Funktion zum Testen der ungefähren Gleichheit verwenden:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
nauer
quelle
3

Da dieser Thread ein wenig in eine allgemeine Diskussion über aktuelle Gleitkommaimplementierungen verzweigt ist, möchte ich hinzufügen, dass es Projekte zur Behebung ihrer Probleme gibt.

Schauen Sie sich zum Beispiel https://posithub.org/ an, das einen Zahlentyp namens posit (und sein Vorgänger unum) zeigt, der eine bessere Genauigkeit mit weniger Bits verspricht. Wenn mein Verständnis richtig ist, behebt es auch die Art der Probleme in der Frage. Sehr interessantes Projekt, die Person dahinter ist ein Mathematiker, Dr. John Gustafson . Das Ganze ist Open Source mit vielen aktuellen Implementierungen in C / C ++, Python, Julia und C # ( https://hastlayer.com/arithmetics ).

Piedone
quelle
3

Es ist eigentlich ziemlich einfach. Wenn Sie ein Basis-10-System haben (wie unser), kann es nur Brüche ausdrücken, die einen Primfaktor der Basis verwenden. Die Primfaktoren von 10 sind 2 und 5. 1/2, 1/4, 1/5, 1/8 und 1/10 können also alle sauber ausgedrückt werden, da die Nenner alle Primfaktoren von 10 verwenden. Im Gegensatz dazu 1 / 3, 1/6 und 1/7 wiederholen sich alle Dezimalstellen, da ihre Nenner einen Primfaktor von 3 oder 7 verwenden. In Binär (oder Basis 2) ist der einzige Primfaktor 2. Sie können also nur Brüche sauber ausdrücken, welche Enthält nur 2 als Primfaktor. In Binärform würden 1/2, 1/4, 1/8 alle sauber als Dezimalstellen ausgedrückt. Während 1/5 oder 1/10 Dezimalstellen wiederholen würden. Während 0,1 und 0,2 (1/10 und 1/5) in einem Basis-10-System saubere Dezimalstellen sind, wiederholen sich Dezimalstellen in dem Basis-2-System, in dem der Computer arbeitet. Wenn Sie diese sich wiederholenden Dezimalstellen berechnen,

Von https://0.30000000000000004.com/

Vlad Agurets
quelle
3

Dezimalzahlen wie 0.1, 0.2und 0.3nicht exakt binär dargestellt codierte Gleitkomma - Typen. Die Summe der Annäherungen für 0.1und 0.2unterscheidet sich von der für verwendeten Näherung 0.3, daher ist die Falschheit von 0.1 + 0.2 == 0.3as hier deutlicher zu sehen:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Ausgabe:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Damit diese Berechnungen zuverlässiger ausgewertet werden können, müssten Sie eine dezimalbasierte Darstellung für Gleitkommawerte verwenden. Der C-Standard spezifiziert solche Typen nicht standardmäßig, sondern als Erweiterung, die in einem technischen Bericht beschrieben wird .

Die _Decimal32, _Decimal64und _Decimal128Typen können auf Ihrem System verfügbar sein (zB GCC unterstützt sie auf ausgewählte Ziele , aber Clang unterstützt diese Funktion nicht auf OS X ).

chqrlie
quelle
1

Math.sum (Javascript) .... Art des Operatorersatzes

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

Die Idee ist, stattdessen Math-Operatoren zu verwenden, um Float-Fehler zu vermeiden

Math.sum erkennt automatisch die zu verwendende Genauigkeit

Math.sum akzeptiert eine beliebige Anzahl von Argumenten

Bortunac
quelle
1
Ich bin mir nicht sicher, ob Sie die Frage " Warum passieren diese Ungenauigkeiten? " Beantwortet haben .
Wai Ha Lee
In
gewisser
Sie beantworten die Frage jedoch immer noch nicht.
Wai Ha Lee
k Sie haben ein Problem damit ... sagen Sie mir, wohin ich es verschieben soll, oder wenn Sie darauf bestehen, dass ich es einfach löschen kann
bortunac vor
0

Ich habe gerade dieses interessante Problem mit Gleitkommazahlen gesehen:

Betrachten Sie die folgenden Ergebnisse:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

Wir können deutlich einen Haltepunkt erkennen, wenn 2**53+1- alles funktioniert gut bis 2**53.

>>> (2**53) - int(float(2**53))
0

Geben Sie hier die Bildbeschreibung ein

Dies geschieht aufgrund des binären Gleitkommaformats mit doppelter Genauigkeit nach IEEE 754: binary64

Von der Wikipedia-Seite für das Gleitkommaformat mit doppelter Genauigkeit :

Binäres Gleitkomma mit doppelter Genauigkeit ist ein häufig verwendetes Format auf PCs, da es trotz seiner Leistung und Bandbreitenkosten einen größeren Bereich als Gleitkomma mit einfacher Genauigkeit bietet. Wie beim Gleitkommaformat mit einfacher Genauigkeit fehlt es im Vergleich zu einem Ganzzahlformat derselben Größe an Genauigkeit für Ganzzahlen. Es ist allgemein einfach als doppelt bekannt. Der IEEE 754-Standard spezifiziert einen Binary64 mit:

  • Vorzeichenbit: 1 Bit
  • Exponent: 11 Bit
  • Signifikante Genauigkeit: 53 Bit (52 explizit gespeichert)

Geben Sie hier die Bildbeschreibung ein

Der reale Wert, der von einem gegebenen 64-Bit-Datum mit doppelter Genauigkeit mit einem gegebenen vorgespannten Exponenten und einem 52-Bit-Bruch angenommen wird, ist

Geben Sie hier die Bildbeschreibung ein

oder

Geben Sie hier die Bildbeschreibung ein

Vielen Dank an @a_guest für den Hinweis.

costargc
quelle
-1

Eine andere Frage wurde als Duplikat zu dieser benannt:

Warum unterscheidet sich das Ergebnis in C ++ cout << xvon dem Wert, für den ein Debugger angezeigt wird x?

Das xin der Frage ist einfloat Variable.

Ein Beispiel wäre

float x = 9.9F;

Der Debugger zeigt 9.89999962, die Ausgabe der coutOperation ist9.9 .

Die Antwort stellt sich als die coutStandardgenauigkeit für herausfloat lautet: Die ist 6, daher wird auf 6 Dezimalstellen gerundet.

Siehe hier als Referenz


quelle
1
IMO - dies hier zu posten war der falsche Ansatz. Ich weiß, dass es frustrierend ist, aber Leute, die eine Antwort auf die ursprüngliche Frage benötigen (anscheinend jetzt gelöscht!), Werden sie hier nicht finden. Wenn Sie wirklich das Gefühl haben, dass Ihre Arbeit gerettet werden sollte, würde ich vorschlagen: 1) nach einem anderen Q zu suchen, das dies tatsächlich beantwortet, 2) eine selbst beantwortete Frage zu erstellen.
Stephen C