Ist die Gleitkomma-Mathematik kaputt?
Betrachten Sie den folgenden Code:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Warum treten diese Ungenauigkeiten auf?
Antworten
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.1
im Standardformat binary64
kann die Darstellung genau so geschrieben werden
0.1000000000000000055511151231257827021181583404541015625
in Dezimalzahl oder0x1.999999999999ap-4
in C99 Hexfloat Notation .
Im Gegensatz dazu die rationale Zahl 0.1
, das ist 1/10
kann geschrieben werden , genau wie
0.1
in Dezimalzahl oder0x1.99999999999999...p-4
in einem Analogon der C99-Hexfloat-Notation, wobei das...
eine endlose Folge von 9en darstellt.
Die Konstanten 0.2
und 0.3
in Ihrem Programm sind auch Annäherungen an ihre wahren Werte. Es kommt vor, dass die am nächsten double
zu 0.2
größer als die rationalen Zahl 0.2
aber , dass die am nächsten double
zu 0.3
kleiner ist als die rationale Zahl 0.3
. Die Summe von 0.1
und 0.2
wird größer als die rationale Zahl 0.3
und 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 0,333333333 ergeben ...
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 Die Gewohnheit, in unserem täglichen Leben ein Basis-2-Zahlensystem zu verwenden, würde man sich diese Zahl sogar ansehen und instinktiv verstehen, dass man dort ankommen könnte, indem man etwas halbiert, es immer wieder halbiert.
** Natürlich werden Gleitkommazahlen nicht genau so im Speicher gespeichert (sie verwenden eine Form der wissenschaftlichen Notation). Es zeigt jedoch den Punkt, dass binäre Gleitkomma-Präzisionsfehler häufig auftreten, weil die "realen" Zahlen, 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 mit einer Dezimalzahl genau 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 abs
ist der absolute Wert. myToleranceValue
muss 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 sind nicht als Toleranzwerte zu verwenden.
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 den Ursprung des Fehlers kennen, können Sie möglicherweise besser verstehen, was in der Software geschieht. Letztendlich hoffe ich, dass dies die Gründe dafür erklärt, 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 insbesondere 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 ist Hardware-Designern jeder Fehler- / Epsilon-Wert gestattet, solange er an letzter Stelle weniger als die Hälfte einer Einheit beträgt und das Ergebnis nur weniger als die Hälfte einer Einheit an der letzten Stelle sein muss Platz für eine Operation. Dies erklärt, warum sich die Fehler bei wiederholten Vorgängen summieren. Für die doppelte Genauigkeit nach 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 Divisionsalgorithmen, die zur Berechnung des Quotienten verwendet werden. 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= 4
Bits (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 Annäherungen an den tatsächlichen Kehrwert und führen ein Fehlerelement 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 Operationen: Abschneiden
Eine weitere Ursache für die Rundungsfehler bei allen Operationen sind die verschiedenen 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, warum 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 Interval Arithmetic kombiniert mit Variationen der IEEE 754-Rundungsmodi , um Rundungsfehler vorherzusagen und zu korrigieren. Aufgrund seines im Vergleich zu anderen Rundungsmodi geringen relativen Fehlers ist die Rundung 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.
Es ist genauso gebrochen wie die Dezimalschreibweise (Basis-10), nur für Basis-2.
Denken Sie zum Verständnis daran, 1/3 als Dezimalwert darzustellen. Es ist unmöglich, genau zu tun! Auf die gleiche Weise kann 1/10 (Dezimalzahl 0,1) nicht genau in Basis 2 (Binärzahl) als "Dezimalwert" dargestellt werden. Ein sich wiederholendes Muster nach dem Dezimalpunkt bleibt für immer bestehen. Der Wert ist nicht exakt und daher können Sie mit normalen Gleitkommamethoden keine exakten Berechnungen durchführen.
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äzisionspizzaschneider 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 Näherung machen, und wenn Sie die Näherung von 0,1 mit der Näherung von 0,2 addieren, erhalten Sie eine ziemlich gute Näherung von 0,3, aber es ist immer noch genau das, eine Nä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 ziemlich viel näher bei 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,300000000000000000084092098
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.)
Gleitkomma-Rundungsfehler. 0,1 kann in Basis-2 aufgrund des fehlenden Primfaktors 5 nicht so genau dargestellt werden wie in Basis-10. Ebenso wie 1/3 eine unendliche Anzahl von Stellen benötigt, um in Dezimalzahlen darzustellen, ist es in Basis-3 "0,1". 0.1 nimmt in Basis-2 eine unendliche Anzahl von Ziffern an, in Basis-10 nicht. Und Computer haben nicht unendlich viel Speicher.
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
... statt:
var result = 0.1 + 0.2; // result === 0.3 returns false
Die Expressions 0.1 + 0.2 === 0.3
kehrt false
in 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: 2550
Cent anstelle von 25.50
Dollar.
1 Douglas Crockford: JavaScript: Die guten Teile : Anhang A - Schreckliche Teile (Seite 105) .
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 :
1
Wenn die Zahl negativ ist,0
andernfalls 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 ist1
.
1 - IEEE 754 ermöglicht das Konzept einer vorzeichenbehafteten Null - +0
und -0
wird 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 beträgt 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 1
s 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 nicht mehr als 1/9, 1/3 oder 1/7 können 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.
Das Konvertieren der Exponenten in Dezimalzahlen, das Entfernen des Versatzes und das erneute Hinzufügen der implizierten 1
(in eckigen Klammern) 0,1 und 0,2 sind:
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.2
in eine binäre IEEE 754-Darstellung (wobei Doppelpunkte 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.2
würde sich ergeben 0.3
: Die letzten paar Bits sind gerundet.
Im Computer gespeicherte Gleitkommazahlen bestehen aus zwei Teilen, einer Ganzzahl und einem Exponenten, zu denen die Basis genommen und mit dem Ganzzahlteil multipliziert wird.
Wenn der Computer in Basis 10 arbeiten 0.1
würde 1 x 10⁻¹
, 0.2
wäre , wäre 2 x 10⁻¹
und 0.3
wäre 3 x 10⁻¹
. Ganzzahlige Mathematik ist einfach und genau, daher führt das Hinzufügen 0.1 + 0.2
offensichtlich 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.5
ist 1 x 2⁻¹
und 0.25
ist 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.1
ist 3602879701896397 x 2⁻⁵⁵
, und die nächste Nummer 0.2
ist 7205759403792794 x 2⁻⁵⁵
; Das Addieren ergibt 10808639105689191 x 2⁻⁵⁵
oder einen exakten Dezimalwert von 0.3000000000000000444089209850062616169452667236328125
. Gleitkommazahlen werden in der Regel zur Anzeige gerundet.
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.
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.
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
Auch wenn Sie 0.2
leicht tippen können FLT_RADIX
und DBL_RADIX
2 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 ohne Zwischenberechnung explizit angeben.
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%).
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, alles 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.
Kurz gesagt , weil:
Gleitkommazahlen können nicht alle Dezimalstellen genau binär darstellen
So wie 3/10 , die nicht existiert in der Basis 10 genau (es wird 3,33 sein ... wiederkehrend), in der gleichen Art und Weise 1/10 nicht in binärer existieren.
Na und? Wie man damit umgeht? Gibt es eine Problemumgehung?
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.2
und 0.7
hier 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 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 als Funktion 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 / 10
das überhaupt nicht funktioniert, obwohl es das gleiche scheint! Ich bevorzuge die erste Lösung, da ich sie als eine Funktion anwenden kann, die den Eingabe-Float in einen genauen Ausgabe-Float umwandelt.
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
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.
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-
decimal
Modul 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 wenn wir eine sehr hohe Präzision benötigen), aber sie lösen dennoch eine große Anzahl von Problemen mit der binären Gleitkomma-Arithmetik.
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, das zu tun - wie eine Umwandlung float
oder double
zu BigDecimal
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.
Addieren der ersten beiden Zahlen manuell oder in einem dezimalen Rechner wie voller Genauigkeit Rechner zeigt die genaue Summe der tatsächlichen Eingänge 0,3000000000000000166533453693773481063544750213623046875 ist.
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.
Kann ich nur hinzufügen; Die Leute nehmen immer an, dass dies ein Computerproblem ist, aber wenn Sie mit Ihren Händen zählen (Basis 10), können Sie es nicht bekommen, (1/3+1/3=2/3)=true
wenn Sie nicht unendlich sind, um 0,333 ... zu 0,333 ... hinzuzufügen, genau wie bei dem (1/10+2/10)!==3/10
Problem 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 ...
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 ist und sich Logik und 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.
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.
Wenn Sie also schreiben float x = 999...
, transformiert der Compiler diese Zahl in eine von der Funktion gedruckte Bitdarstellung xx
, sodass die von der Funktion gedruckte Summe yy
der 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 bc
die Summe der vom Hauptprogramm ausgegebenen Begriffe gedruckt. Man kann diese Summe auch in Python repl
oder ä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 scale
Faktor einzustellen bc
.
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 scale
Faktor auf 15 gesetzt. Mathematisch gesehen scheint es mit unendlicher Genauigkeit 1.000.000.000 zu sein.
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 +/- Unendlichkeit 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.
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
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.00000000
von 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. Zum Beispiel, anstatt 1/10 zu speichern, wie 0.0001100
wir 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.
Dezimalzahlen wie 0.1
, 0.2
und 0.3
nicht exakt binär dargestellt codierte Gleitkomma - Typen. Die Summe der Annäherungen für 0.1
und 0.2
unterscheidet sich von der für verwendeten Näherung 0.3
, daher ist die Falschheit von 0.1 + 0.2 == 0.3
as 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
, _Decimal64
und _Decimal128
Typen 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 ).
Da sich dieser Thread ein wenig in eine allgemeine Diskussion über aktuelle Gleitkomma-Implementierungen verzweigte, möchte ich hinzufügen, dass es Projekte zur Behebung ihrer Probleme gibt.
Schau es dir an https://posithub.org/Beispiel: Ein Zahlentyp namens posit (und sein Vorgänger unum), 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).
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 enthalten, wiederholen sich Dezimalstellen in dem Basis-2-System, in dem der Computer arbeitet. Wenn Sie diese sich wiederholenden Dezimalstellen berechnen, erhalten Sie Reste Diese werden übertragen, wenn Sie die Basis-2-Nummer (Binärzahl) des Computers in eine besser lesbare Basis-10-Nummer umwandeln.
Von https://0.30000000000000004.com/
Normale Arithmetik ist Basis-10, also stehen Dezimalstellen für Zehntel, Hundertstel usw. Wenn Sie versuchen, eine Gleitkommazahl in binärer Basis-2-Arithmetik darzustellen, haben Sie es mit Hälften, Vierteln, Achteln usw. zu tun.
In der Hardware werden Gleitkommawerte als ganzzahlige Mantissen und Exponenten gespeichert. Mantisse repräsentiert die signifikanten Ziffern. Exponent ist wie wissenschaftliche Notation, verwendet jedoch eine Basis von 2 anstelle von 10. Zum Beispiel würde 64,0 mit einer Mantisse von 1 und Exponent von 6 dargestellt. 0,125 würde mit einer Mantisse von 1 und einem Exponenten von -3 dargestellt.
Gleitkomma-Dezimalstellen müssen negative Potenzen von 2 addieren
0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d
und so weiter.
Es ist üblich, ein Fehlerdelta anstelle von Gleichheitsoperatoren zu verwenden, wenn mit Gleitkomma-Arithmetik gearbeitet wird. Statt
if(a==b) ...
du würdest verwenden
delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...
Gleitkommazahlen werden auf Hardwareebene als Bruchteile von Binärzahlen (Basis 2) dargestellt. Zum Beispiel der Dezimalbruch:
0.125
hat den Wert 1/10 + 2/100 + 5/1000 und auf die gleiche Weise den binären Bruch:
0.001
hat den Wert 0/2 + 0/4 + 1/8. Diese beiden Brüche haben den gleichen Wert. Der einzige Unterschied besteht darin, dass der erste ein Dezimalbruch und der zweite ein binärer Bruch ist.
Leider können die meisten Dezimalbrüche keine exakte Darstellung in binären Brüchen haben. Daher werden die von Ihnen angegebenen Gleitkommazahlen im Allgemeinen nur an binäre Brüche angenähert, die in der Maschine gespeichert werden sollen.
Das Problem ist in Basis 10 leichter zu lösen. Nehmen Sie zum Beispiel den Bruch 1/3. Sie können es auf einen Dezimalbruch approximieren:
0.3
oder besser,
0.33
oder besser,
0.333
usw. Egal wie viele Dezimalstellen Sie schreiben, das Ergebnis ist nie genau 1/3, aber es ist eine Schätzung, die immer näher kommt.
Unabhängig davon, wie viele Dezimalstellen der Basis 2 Sie verwenden, kann der Dezimalwert 0,1 nicht genau als binärer Bruch dargestellt werden. In Basis 2 ist 1/10 die folgende periodische Zahl:
0.0001100110011001100110011001100110011001100110011 ...
Halten Sie bei einer endlichen Anzahl von Bits an, und Sie erhalten eine Annäherung.
Für Python werden auf einer typischen Maschine 53 Bit für die Genauigkeit eines Gleitkommas verwendet, sodass der Wert, der bei der Eingabe der Dezimalzahl 0,1 gespeichert wird, der binäre Bruch ist.
0.00011001100110011001100110011001100110011001100110011010
Das ist nah, aber nicht genau gleich 1/10.
Es ist leicht zu vergessen, dass der gespeicherte Wert eine Annäherung an den ursprünglichen Dezimalbruch ist, da Floats im Interpreter angezeigt werden. Python zeigt nur eine dezimale Annäherung an den in Binär gespeicherten Wert an. Wenn Python den wahren Dezimalwert der für 0,1 gespeicherten binären Näherung ausgeben würde, würde es Folgendes ausgeben:
>>> 0.1
0.1000000000000000055511151231257827021181583404541015625
Dies sind viel mehr Dezimalstellen als die meisten Leute erwarten würden, daher zeigt Python einen gerundeten Wert an, um die Lesbarkeit zu verbessern:
>>> 0.1
0.1
Es ist wichtig zu verstehen, dass dies in Wirklichkeit eine Illusion ist: Der gespeicherte Wert ist nicht genau 1/10, es ist einfach auf dem Display, dass der gespeicherte Wert gerundet wird. Dies wird deutlich, sobald Sie arithmetische Operationen mit folgenden Werten ausführen:
>>> 0.1 + 0.2
0.30000000000000004
Dieses Verhalten ist der Natur der Gleitkommadarstellung der Maschine inhärent: Es ist weder ein Fehler in Python noch ein Fehler in Ihrem Code. Sie können dieselbe Art von Verhalten in allen anderen Sprachen beobachten, die Hardware-Unterstützung für die Berechnung von Gleitkommazahlen verwenden (obwohl einige Sprachen den Unterschied standardmäßig nicht oder nicht in allen Anzeigemodi sichtbar machen).
Eine weitere Überraschung ist dieser inhärent. Wenn Sie beispielsweise versuchen, den Wert 2.675 auf zwei Dezimalstellen zu runden, erhalten Sie
>>> round (2.675, 2)
2.67
Die Dokumentation für das Grundelement round () gibt an, dass es auf den nächsten Wert von Null weg gerundet wird. Da der Dezimalbruch genau auf halber Strecke zwischen 2,67 und 2,68 liegt, sollten Sie mit einer binären Annäherung von 2,68 rechnen. Dies ist jedoch nicht der Fall, da der Dezimalbruch 2.675, wenn er in einen Gleitkommawert umgewandelt wird, durch eine Näherung gespeichert wird, deren genauer Wert ist:
2.67499999999999982236431605997495353221893310546875
Da die Annäherung etwas näher an 2,67 als an 2,68 liegt, ist die Rundung nach unten gerichtet.
Wenn Sie sich in einer Situation befinden, in der es darauf ankommt, Dezimalzahlen auf die Hälfte abzurunden, sollten Sie das Dezimalmodul verwenden. Übrigens bietet das Dezimalmodul auch eine bequeme Möglichkeit, den genauen Wert zu "sehen", der für einen Float gespeichert ist.
>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')
Eine weitere Konsequenz der Tatsache, dass 0,1 nicht genau in 1/10 gespeichert ist, ist, dass die Summe von zehn Werten von 0,1 auch nicht 1,0 ergibt:
>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999
Die Arithmetik binärer Gleitkommazahlen hält viele solcher Überraschungen bereit. Das Problem mit "0.1" wird unten im Abschnitt "Darstellungsfehler" ausführlich erläutert. Eine vollständigere Liste solcher Überraschungen finden Sie unter Die Gefahren des Gleitkommas.
Es ist wahr, dass es keine einfache Antwort gibt, aber seien Sie nicht übermäßig misstrauisch gegenüber schwebenden Virtulazahlen! Fehler in Python bei Gleitkommazahlenoperationen sind auf die zugrunde liegende Hardware zurückzuführen, und auf den meisten Computern beträgt die Anzahl nicht mehr als 1 zu 2 ** 53 pro Operation. Dies ist für die meisten Aufgaben mehr als erforderlich. Beachten Sie jedoch, dass es sich nicht um Dezimaloperationen handelt und dass bei jeder Operation mit Gleitkommazahlen möglicherweise ein neuer Fehler auftritt.
Obwohl pathologische Fälle vorliegen, erhalten Sie für die meisten gängigen Anwendungsfälle das erwartete Ergebnis am Ende, indem Sie einfach auf die Anzahl der Dezimalstellen aufrunden, die auf dem Display angezeigt werden sollen. Informationen zur genauen Steuerung der Anzeige von Floats finden Sie unter String-Formatierungssyntax für die Formatierungsspezifikationen der str.format () -Methode.
In diesem Teil der Antwort wird das Beispiel von "0.1" ausführlich erläutert und gezeigt, wie Sie eine genaue Analyse dieser Art von Fall selbst durchführen können. Wir gehen davon aus, dass Sie mit der binären Darstellung von Gleitkommazahlen vertraut sind. Der Begriff Darstellungsfehler bedeutet, dass die meisten Dezimalbrüche nicht genau binär dargestellt werden können. Dies ist der Hauptgrund, warum Python (oder Perl, C, C ++, Java, Fortran und viele andere) das genaue Ergebnis normalerweise nicht dezimal anzeigt:
>>> 0.1 + 0.2
0.30000000000000004
Warum ? 1/10 und 2/10 sind nicht genau in binären Brüchen darstellbar. Alle Maschinen von heute (Juli 2010) folgen jedoch dem IEEE-754-Standard für die Arithmetik von Gleitkommazahlen. und die meisten Plattformen verwenden eine "IEEE-754-Doppelgenauigkeit", um Python-Floats darzustellen. Doppelte Genauigkeit IEEE-754 verwendet eine Genauigkeit von 53 Bit. Beim Lesen versucht der Computer, 0,1 in den nächsten Bruchteil der Form J / 2 ** N zu konvertieren, wobei J eine Ganzzahl von genau 53 Bit ist. Umschreiben:
1/10 ~ = J / (2 ** N)
im :
J ~ = 2 ** N / 10
Wenn man bedenkt, dass J genau 53 Bit ist (also> = 2 ** 52, aber <2 ** 53), ist der bestmögliche Wert für N 56:
>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793
56 ist also der einzig mögliche Wert für N, der genau 53 Bits für J übrig lässt. Der bestmögliche Wert für J ist daher dieser gerundete Quotient:
>>> q, r = divmod (2 ** 56, 10)
>>> r
6
Da der Übertrag größer als die Hälfte von 10 ist, wird die beste Annäherung durch Aufrunden erhalten:
>>> q + 1
7205759403792794
Daher ist die bestmögliche Annäherung für 1/10 in "IEEE-754 doppelte Genauigkeit" diese über 2 ** 56, dh:
7205759403792794/72057594037927936
Da die Rundung nach oben erfolgt ist, ist das Ergebnis tatsächlich etwas größer als 1/10. Wenn wir nicht aufgerundet hätten, wäre der Quotient etwas kleiner als 1/10 gewesen. Aber auf keinen Fall ist es genau 1/10!
Der Computer "sieht" also nie 1/10: Was er sieht, ist der oben angegebene genaue Bruch, die beste Annäherung unter Verwendung der Gleitkommazahlen mit doppelter Genauigkeit aus dem "IEEE-754":
>>>. 1 * 2 ** 56
7205759403792794.0
Wenn wir diesen Bruch mit 10 ** 30 multiplizieren, können wir die Werte seiner 30 Dezimalstellen mit starkem Gewicht beobachten.
>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L
Dies bedeutet, dass der genaue Wert, der auf dem Computer gespeichert ist, ungefähr dem Dezimalwert 0,1000000000000000055551115123125 entspricht. In Versionen vor Python 2.7 und Python 3.1 hat Python diese Werte auf 17 signifikante Dezimalstellen gerundet und "0.10000000000000001" angezeigt. In aktuellen Versionen von Python ist der angezeigte Wert der Wert, dessen Bruchteil so kurz wie möglich ist, während er bei der Rückkonvertierung in eine Binärdatei genau dieselbe Darstellung liefert, indem einfach "0.1" angezeigt wird.
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
Dies geschieht aufgrund des binären Gleitkommaformats mit doppelter Genauigkeit: binär64
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)
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
oder
Vielen Dank an @a_guest für den Hinweis.