Betrachten Sie den folgenden Code:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Warum treten diese Ungenauigkeiten auf?
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 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 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 dürfen nicht als Toleranzwerte verwendet werden.
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.
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.
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.
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= 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 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.
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.
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.
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.
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.
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.
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.
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.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:
1
Wenn die Zahl negativ ist, 0
andernfalls 1 .1.
immer 2 weggelassen, da das höchstwertige Bit eines Binärwerts ist 1
.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 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 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 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.2
in 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.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 dem 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.
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 explizit ohne Zwischenberechnung 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%).
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.
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).
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
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, 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 / 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 Eingangs-Float in einen genauen Ausgangs-Float umwandelt.
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.
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 float
oder double
inBigDecimal
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.
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 decimal
Modul und Javas BigDecimal
Klasse , 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 fractions
Modul und Apache Common BigFraction
Klasse . 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.
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)=true
wenn Sie 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 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.
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 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 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 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 +/- 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.
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. Also 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.
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
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 ).
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,
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 ).
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
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 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)
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.
Eine andere Frage wurde als Duplikat zu dieser benannt:
Warum unterscheidet sich das Ergebnis in C ++ cout << x
von dem Wert, für den ein Debugger angezeigt wird x
?
Das x
in der Frage ist einfloat
Variable.
Ein Beispiel wäre
float x = 9.9F;
Der Debugger zeigt 9.89999962
, die Ausgabe der cout
Operation ist9.9
.
Die Antwort stellt sich als die cout
Standardgenauigkeit für herausfloat
lautet: Die ist 6, daher wird auf 6 Dezimalstellen gerundet.
Siehe hier als Referenz