it-swarm.com.de

Ist 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?

2706
Cato Johnston

Binär Gleitkomma Mathe ist wie folgt. In den meisten Programmiersprachen basiert es auf dem IEEE 754-Standard . JavaScript verwendet eine 64-Bit-Fließkommadarstellung, die der von Java entspricht (double). Der Kern des Problems besteht darin, dass Zahlen in diesem Format als ganze Zahl multipliziert mit einer Zweierpotenz dargestellt werden. Rationale Zahlen (wie _0.1_, was _1/10_ ist), deren Nenner keine Zweierpotenz ist, können nicht genau dargestellt werden.

Für _0.1_ im Standardformat _binary64_ kann die Darstellung genauso geschrieben werden

  • _0.1000000000000000055511151231257827021181583404541015625_ in Dezimalzahl oder
  • _0x1.999999999999ap-4_ in C99-Hexfloat-Notation .

Im Gegensatz dazu kann die rationale Zahl _0.1_, die _1/10_ ist, genauso geschrieben werden

  • _0.1_ in Dezimalzahl oder
  • _0x1.99999999999999...p-4_ in einem Analogon der C99-Hexfloat-Notation, wobei _..._ eine endlose Folge von 9 darstellt.

Die Konstanten _0.2_ und _0.3_ in Ihrem Programm sind ebenfalls Annäherungen an ihre wahren Werte. Es kommt vor, dass das double zu _0.2_ größer ist als die rationale Zahl _0.2_, aber dass das double zu _0.3_ kleiner ist als die rationale Zahl _0.3_. Die Summe von _0.1_ und _0.2_ ist größer als die rationale Zahl _0.3_ und stimmt daher nicht mit der Konstanten in Ihrem Code überein.

Eine ziemlich umfassende Behandlung von Fließkomma-Arithmetikproblemen ist Was jeder Informatiker über Fließkomma-Arithmetik wissen sollte . Eine einfachere Erklärung finden Sie unter floating-point-gui.de .

Seite Hinweis: Alle Positionszahlensysteme (Basis-N) teilen dieses Problem mit Präzision

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

Sie sind gerade auf eine Zahl gestoßen (3/10), die mit dem Dezimalsystem einfach 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ärzahl sieht es genauso gut aus wie ein 10.000stel in Dezimalzahl (0,0001) ** - wenn wir dabei wären Wenn man es sich zur Gewohnheit macht, in unserem täglichen Leben ein Zahlensystem zur Basis 2 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 so gespeichert (sie verwenden eine Form der wissenschaftlichen Notation). Dies verdeutlicht jedoch, 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 ein 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 exakt 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 Gleichheitstests auch durch Vergleiche ersetzen, die ein gewisses Maß an Toleranz zulassen. Dies bedeutet:

Mach nicht mach if (float1 == float2) { ... }

Führen Sie stattdessen if (Math.Abs(float1 - float2) < myToleranceValue) { ... } aus.

myToleranceValue muss für Ihre spezielle Anwendung ausgewählt werden - und es hängt in hohem Maße davon ab, wie viel "Wackelspielraum" Sie bereit sind, zuzulassen, und wie hoch die größte Zahl sein wird, die Sie vergleichen möchten (aufgrund von Genauigkeitsverlusten) Probleme). Achten Sie auf "double.Epsilon" -Stilkonstanten in der Sprache Ihrer Wahl (Number.EPSILON in Javascript). Diese sind nicht als Toleranzwerte zu verwenden.

Weitere Informationen zu Toleranzen:

(Schamlose Eigenwerbung durch einen Redakteur - Entschuldigung für den Hijack)

Ich habe unter https://dev.to/alldanielscott/how-to-compare-numbers-correctly -in-Javascript-1l4i

2048
Brian R. Bondy

Die Perspektive eines Hardware-Designers

Ich glaube, ich sollte die Perspektive eines Hardware-Designers hinzufügen, da ich Gleitkomma-Hardware entwerfe und baue. Wenn Sie wissen, woher der Fehler stammt, können Sie möglicherweise besser verstehen, was in der Software vor sich geht. Letztendlich hoffe ich, dass dies die Gründe dafür erklärt, warum Gleitkommafehler auftreten und sich im Laufe der Zeit zu häufen scheinen.

1. Übersicht

Aus technischer Sicht weisen die meisten Gleitkommaoperationen einige Fehler auf, da die Hardware, die die Gleitkomma-Berechnungen durchführt, letztendlich nur einen Fehler von weniger als der Hälfte einer Einheit aufweisen muss. Aus diesem Grund stoppt viel Hardware bei einer Genauigkeit, die nur erforderlich ist, um für eine einzelne Operation , die beim Floaten besonders problematisch ist, einen Fehler von weniger als der Hälfte einer Einheit zu erzielen Punktteilung. 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 kann nicht garantiert werden, dass wiederholte Vorgänge zu einem wünschenswerten Fehler führen, da sich die Fehler mit der Zeit summieren.

2. Standards

Die meisten Prozessoren folgen dem Standard IEEE-754 , 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. Das Folgende wird jedoch den normalisierten Modus von IEEE-754 abdecken, der der typische Betriebsmodus ist.

Nach dem IEEE-754-Standard dürfen Hardware-Designer jeden Wert für error/epsilon verwenden, sofern dieser weniger als die Hälfte einer Einheit beträgt und das Ergebnis nur weniger als die Hälfte einer Einheit in der letzten Einheit sein muss Platz für eine Operation. Dies erklärt, warum sich die Fehler summieren, wenn es wiederholte Operationen gibt. Für IEEE-754 mit doppelter Genauigkeit 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 näher erläutert.

3. Ursache für Rundungsfehler 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, d. H. Jeder Zyklus berechnet einige Bits des Quotienten, bis die gewünschte Genauigkeit erreicht ist, was für IEEE-754 alles ist, was einen Fehler von weniger als einer Einheit an letzter Stelle aufweist. Die Kehrwerttabelle von Y (1/Y) ist bekannt als die Quotientenauswahltabelle (QST) in der langsamen Division, und die Größe in Bits der Quotientenauswahltabelle ist gewöhnlich die Breite der Basis 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 zuzüglich einiger Schutzbits k, wobei k>=2. Eine typische Quotientenauswahltabelle für einen Teiler, der jeweils 2 Bits des Quotienten berechnet (Basis 4), wäre also 2+2= 4 Bits (plus einige optionale Bits).

3.1 Division Rounding Error: Approximation des Reziproken

Welche Hin- und Herbewegungen 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 Hin- und Herbewegungen Näherungen des tatsächlichen Hin- und Herbewegens und führen ein Fehlerelement ein. Sowohl langsame als auch schnelle Divisionsmethoden berechnen den Quotienten iterativ, dh es wird eine bestimmte Anzahl von Bits des Quotienten pro Schritt berechnet, dann wird das Ergebnis von der Dividende subtrahiert und der Teiler wiederholt die Schritte, bis der Fehler kleiner als die Hälfte von eins ist Einheit an letzter Stelle. Langsame Teilungsmethoden berechnen eine feste Anzahl von Stellen des Quotienten in jedem Schritt und sind in der Regel kostengünstiger zu erstellen, und schnelle Teilungsmethoden berechnen eine variable Anzahl von Stellen pro Schritt und sind in der Regel teurer zu erstellen. Der wichtigste Teil der Teilungsmethoden besteht darin, dass die meisten von ihnen auf der wiederholten Multiplikation mit einer Approximation eines Reziprokwerts beruhen, sodass sie fehleranfällig sind.

4. Rundungsfehler bei anderen Operationen: Abschneiden

Eine weitere Ursache für die Rundungsfehler in allen Operationen sind die unterschiedlichen Kürzungsmodi der endgültigen Antwort, die IEEE-754 zulässt. Es gibt Trunkate, Round-gegen-Null, Round-to-Nearest (Standard) Round-down und Round-up. Alle Methoden führen für eine einzelne Operation an letzter Stelle ein Fehlerelement von weniger als einer Einheit ein. Im Laufe der Zeit und bei wiederholten Operationen addiert sich das Abschneiden ebenfalls zum resultierenden Fehler. Dieser Kürzungsfehler ist besonders problematisch bei der Exponentiation, die eine Form wiederholter Multiplikation beinhaltet.

5. Wiederholte Operationen

Da die Hardware, die die Gleitkommaberechnungen durchführt, nur ein Ergebnis mit einem Fehler von weniger als einer halben Einheit an letzter Stelle für eine einzelne Operation liefern muss, wächst der Fehler über wiederholte Operationen, wenn er nicht überwacht wird. Dies ist der Grund, warum Mathematiker bei Berechnungen, die einen begrenzten Fehler erfordern, Methoden verwenden, wie beispielsweise die Verwendung des Abstands zum nächsten gerade Ziffer an letzter Stelle von IEEE-754, da die Fehler im Laufe der Zeit auftreten heben sich eher gegenseitig auf und Intervallarithmetik kombiniert mit Variationen der IEEE 754-Rundungsmodi , um Rundungsfehler vorherzusagen und zu korrigieren. Aufgrund des 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, Rundung auf nächste gerade Ziffer an letzter Stelle , einen Fehler von weniger als der Hälfte einer Einheit an letzter Stelle für eine Operation garantiert. Die alleinige Verwendung von Kürzung, Abrundung und Abrundung kann zu einem Fehler führen, der größer als die Hälfte einer Einheit am letzten Ort, aber kleiner als eine Einheit am letzten Ort ist. 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 der Kürzung in der Hardware und der Kürzung eines Kehrwerts im Fall einer Division. Da der IEEE-754-Standard für eine einzelne Operation nur an letzter Stelle einen Fehler von weniger als der Hälfte einer Einheit erfordert, summieren sich die Gleitkommafehler bei wiederholten Operationen, sofern sie nicht korrigiert werden.

570
KernelPanik

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

427
Joel Coehoorn

Die meisten Antworten hier sprechen diese Frage sehr trocken und technisch an. 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 Pizzascheiben genau in zwei Hälften schneiden 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, halbieren Sie diese und halbieren Sie jedes Mal die kleinste Scheibe. Sie können die Halbierung 53 Mal vorher durchführen Die Scheibe ist zu klein für hochpräzise Fähigkeiten. Zu diesem Zeitpunkt können Sie dieses sehr dünne Segment nicht mehr halbieren, sondern müssen es so wie es ist entweder einschließen oder ausschließen.

Wie würden Sie nun alle Scheiben so zerkleinern, 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 auszuarbeiten. 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, nämlich, dass es keine Möglichkeit gibt, ein genaues Zehntel oder Fünftel der Pizza mit diesen Scheiben zusammenzusetzen, egal wie fein du schneidest sie. Sie können eine ziemlich gute Näherung durchführen, 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 (mit dieser Genauigkeit können Sie Ihre Pizza 53-mal halbieren) sind die Zahlen sofort kleiner und größer als 0,1: 0,09999999999999999167332731531132594682276248931884765625 und 0,10000000000000000005551115410156. Letzteres ist um einiges näher an 0.1 als das erstere, sodass ein numerischer Parser bei einer Eingabe von 0.1 das letztere bevorzugt.

(Der Unterschied zwischen diesen beiden Zahlen ist das "kleinste Stück", für das wir uns entscheiden müssen, entweder einzufügen, was eine Aufwärtsverzerrung einführt, oder auszuschließen, was eine Abwärtsverzerrung einführt. Der Fachbegriff für dieses kleinste Stück ist ein lp .)

Im Fall von 0,2 sind die Zahlen alle gleich, nur um den Faktor 2 erhöht. 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 leicht nach oben gerichtet sind. Wenn wir genug von diesen Verzerrungen hinzufügen, werden sie die Zahl immer weiter von dem wegschieben, 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ächstliegende Zahl ist bis 0,3.

Insbesondere ist 0,1 + 0,2 wirklich 0,1000000000000000055511151231257827021181583404541015625 + 0,200000000000000011102230246251565404236316680908203125 = 0,3000000000000000090408920985006261616945875999.


P.S. Einige Programmiersprachen bieten auch Pizzaschneider an, die Scheiben in genaue Zehntel aufteilen . Obwohl solche Pizzaschneider selten sind, sollten Sie sie verwenden, wenn es wichtig ist, genau ein Zehntel oder ein Fünftel einer Scheibe zu erhalten, wenn Sie Zugriff auf eine haben.

(Ursprünglich auf Quora gepostet.)

281

Gleitkomma-Rundungsfehler. 0.1 kann in der Basis 2 nicht so genau dargestellt werden wie in der Basis 10, da der Primfaktor 5 fehlt. So wie 1/3 eine unendliche Anzahl von Ziffern benötigt, um dezimal dargestellt zu werden, ist in der Basis 3 "0.1". 0.1 hat eine unendliche Anzahl von Stellen in der Basis 2, nicht in der Basis 10. Und Computer haben nicht unendlich viel Speicher.

207

Zusätzlich zu den anderen richtigen Antworten können Sie Ihre Werte skalieren, um Probleme mit der Gleitkomma-Arithmetik zu vermeiden.

Zum Beispiel:

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

... anstatt:

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

Der Ausdruck 0.1 + 0.2 === 0.3 gibt false in JavaScript zurück, aber glücklicherweise ist die Ganzzahlarithmetik in Gleitkommazahlen genau, sodass durch Skalierung Fehler bei der Dezimaldarstellung vermieden werden können.

Als praktisches Beispiel wird empfohlen, um Gleitkommaprobleme zu vermeiden, bei denen Genauigkeit an erster Stelle steht1 Um Geld 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) .

116
Daniel Vassallo

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

Präambel

Eine IEEE 754-Binär-Gleitkommaformat mit doppelter Genauigkeit (binary64) Zahl steht für eine Zahl des Formulars

value = (-1) ^ s * (1.m51m50... m2m1m)2 * 2e-1023

in 64 Bit:

  • Das erste Bit ist das Vorzeichenbit : 1, wenn die Zahl negativ ist, 0, ansonsten1.
  • Die nächsten 11 Bits sind Exponent , was Offset von 1023 ist. Mit anderen Worten, nach dem Lesen der Exponentenbits von einer Zahl mit doppelter Genauigkeit muss 1023 subtrahiert werden, um zu erhalten die Kraft von zwei.
  • Die verbleibenden 52 Bits sind Hochkomma (oder Mantisse). In der Mantisse ist ein 'impliziter' 1. immer2 weggelassen, da das höchstwertige Bit eines Binärwerts 1 ist.

1 - IEEE 754 erlaubt das Konzept einer vorzeichenbehafteten Null - +0 und -0 werden unterschiedlich behandelt: 1 / (+0) ist positive Unendlichkeit; 1 / (-0) ist eine negative Unendlichkeit. Bei Nullwerten sind die Mantissen- und Exponentenbits alle Null. Hinweis: Nullwerte (+0 und -0) werden ausdrücklich nicht als normal eingestuft2.

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


Eine Zahl mit doppelter Genauigkeit in eine Binärzahl 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-C # -Code, um die IEEE 754-Darstellung für eine Zahl mit doppelter Genauigkeit zu erhalten (I separate) 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 kommen: 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 zwischen den drei Teilen):

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

Beachten Sie, dass die Mantisse aus wiederkehrenden Ziffern von 0011 besteht. Dies ist der Schlüssel , warum die Berechnungen fehlerhaft sind - 0.1, 0.2 und 0.3 können nicht in binärer Form dargestellt werden Genau in einer endlichen Anzahl von Binärbits können 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ärdarstellung um 52 Stellen nach rechts verschieben können (ähnlich wie bei 10)-3 * 1,23 == 10-5 * 123). Auf diese Weise können wir die Binärdarstellung als den exakten Wert darstellen, den sie in der Form a * 2 darstelltp. Dabei ist 'a' eine ganze Zahl.

Konvertieren der Exponenten in Dezimalzahlen, Entfernen des Offsets und erneutes Hinzufügen des implizierten 1 (in eckigen Klammern): 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, d. H .:

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 von der Form 2 istn * 1. {bbb} Wir erhöhen den Exponenten um eins und verschieben den Dezimalpunkt (), um Folgendes zu erhalten:

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

Die Mantisse enthält jetzt 53 Bits (die 53. befindet sich in der oberen Zeile in eckigen Klammern). Der Standardwert Rundungsmodus für IEEE 754 ist ' Auf nächstgelegenes runden' - dh, wenn eine Zahl x zwischen zwei Werte fällt a und b wird der Wert gewählt, bei dem das niedrigstwertige Bit Null ist.

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 von Null b . Die Summe lautet also:

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

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

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

TL; DR

Schreiben von 0.1 + 0.2 in einer IEEE 754-Binärdarstellung (mit Doppelpunkten, die die drei Teile trennen) und Vergleichen mit 0.3, dies ist (ich habe die einzelnen Bits in eckige Klammern gesetzt):

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

Diese Werte werden zurück in Dezimalzahlen konvertiert:

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

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

Das Vergleichen der letzten Bits einer Gleitkommazahl ist von Natur aus gefährlich, da jeder, der das berühmte " Was jeder Informatiker über Gleitkomma-Arithmetik wissen sollte " liest (das alle Hauptteile dieser Antwort abdeckt) ) wirst wissen.

Die meisten Taschenrechner verwenden zusätzliche Schutzziffern , um dieses Problem zu umgehen. So würde 0.1 + 0.20.3 ergeben: Die letzten paar Bits werden gerundet.

97
Wai Ha Lee

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 würde, wäre 0.11 x 10⁻¹, 0.2 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 von 0.1 + 0.2 offensichtlich zu 0.3.

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

Das Problem ergibt sich aus Zahlen, die genau in der Basis 10, aber nicht in der Basis 2 dargestellt werden können. Diese Zahlen müssen auf das nächste Äquivalent gerundet werden. Unter der Annahme des sehr gebräuchlichen IEEE-64-Bit-Gleitkommaformats ist die 0.1 am nächsten gelegene Zahl 3602879701896397 x 2⁻⁵⁵ und die 0.2 am nächsten gelegene Zahl 7205759403792794 x 2⁻⁵⁵; Wenn Sie diese addieren, erhalten Sie 10808639105689191 x 2⁻⁵⁵ oder einen exakten Dezimalwert von 0.3000000000000000444089209850062616169452667236328125. Gleitkommazahlen werden in der Regel für die Anzeige gerundet.

54
Mark Ransom

Gleitkomma-Rundungsfehler. Von Was jeder Informatiker über Gleitkomma-Arithmetik wissen sollte :

Um unendlich viele reelle Zahlen in eine endliche Anzahl von Bits zu zerlegen, ist eine ungefähre Darstellung erforderlich. 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 oft gerundet werden, um wieder in seine endliche Darstellung zu passen. Dieser Rundungsfehler ist das charakteristische Merkmal der Gleitkommaberechnung.

46
Brett Daniel

Mein Workaround:

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

Genauigkeit bezieht sich auf die Anzahl der Stellen, die Sie nach dem Komma beim Hinzufügen beibehalten möchten.

33
Justineo

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

Nicht alle Zahlen können durch Gleitkommazahlen / Doppelzahlen dargestellt werden Die Zahl "0.2" wird im Gleitkomma-Standard IEEE754 mit einfacher Genauigkeit als "0.200000003" dargestellt.

Modell zum Speichern von reellen Zahlen unter der Haube stellen Gleitzahlen dar als

enter image description here

Obwohl Sie 0.2 leicht eingeben können, sind FLT_RADIX und DBL_RADIX 2; 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 ein bisschen schwierig, solche Zahlen genau darzustellen. Auch wenn Sie diese Variable explizit ohne Zwischenberechnung angeben.

29
bruziuz

Einige Statistiken zu dieser berühmten Frage mit doppelter Genauigkeit.

Wenn alle Werte ( a + b ) in Schritten von 0,1 (von 0,1 bis 100) addiert werden, haben wir ~ 15% Genauigkeitschance) Fehler. Beachten Sie, dass der Fehler zu geringfügig 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)

Beim Subtrahieren aller Werte ( a - b wobei a> b ) mit Ein Schritt von 0,1 (von 100 auf 0,1) ergibt ~ 34% Wahrscheinlichkeit eines Präzisionsfehlers. 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 riesig. Verwenden Sie daher immer BigDecimal, wenn Präzision von großer Bedeutung ist. Bei 2 Nachkommastellen (Schritt 0.01) verschlechtert sich die Situation etwas (18% und 36%).

28
Kostas Chalkias

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

Zusammenfassung

Die Fließkomma-Arithmetik ist genau, passt aber leider nicht zu unserer üblichen 10-Basis-Zahlendarstellung, so dass sich herausstellt, dass wir oft geben Es ist eine Eingabe, die leicht von dem abweicht, 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 der Basis darstellbaren Bruch2. Wenn Sie dies mit FP versucht hätten, wäre Ihre 0,01 geringfügig niedriger gewesen, und die einzige Möglichkeit, 25 von ihnen zu einer Nizza-Genauigkeit von 0,25 zu addieren, hätte eine lange Kausalkette mit Schutzbits und Rundung erfordert. Es ist schwer vorherzusagen, also erheben wir unsere Hände und sagen "FP ist ungenau", aber das ist nicht wirklich wahr.

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

Wie ist das passiert?

Wenn wir dezimal schreiben, ist jeder Bruch (insbesondere jede endende Dezimalstelle) eine rationale Zahl der Form

a/(2n x 5m)

Im Binärformat erhalten wir nur die 2n Begriff, das heißt:

a/2n

In Dezimalzahlen können wir also nicht darstellen 1/3. Da die Basis 10 2 als Primfaktor enthält, kann jede Zahl, die wir als binären Bruch schreiben können , auch als Bruch zur Basis 10 geschrieben werden. Wir schreiben jedoch kaum etwas als Basis10 Bruch ist 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, weil sie 1/4, 1/2 und 3/4 sind, alle Zahlen mit einem Primfaktor, der nur die 2 verwendetn Begriff.

In der Basis10 wir können nicht darstellen 1/3. Aber im Binären können wir nicht tun 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 im Binärformat.

Umgang damit

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

Auch bei realen Zahlenproblemen (die Probleme, für die FP bei frühen, furchtbar teuren Computern erfunden wurde) sind die physikalischen Konstanten des Universums und alle anderen Messungen nur einer relativ kleinen Anzahl signifikanter Figuren bekannt , so dass der gesamte Problemraum ohnehin "ungenau" war. FP "Genauigkeit" ist in dieser Art von Anwendung kein Problem.

Das ganze Problem entsteht wirklich, wenn Leute versuchen, FP für das Bohnenzählen zu verwenden. Dafür funktioniert es, aber nur, wenn Sie sich an ganzzahlige Werte halten, die den Sinn ihrer Verwendung zunichte machen. Deshalb haben wir all diese Dezimalbruch-Softwarebibliotheken.

Ich liebe die Pizza-Antwort von Chris , weil sie das eigentliche Problem beschreibt, nicht nur die übliche Handbewegung über "Ungenauigkeit". Wenn FP einfach "ungenau" wäre, könnten wir das beheben und hätten es vor Jahrzehnten getan. Der Grund dafür ist, dass das FP -Format kompakt und schnell ist und es der beste Weg ist, viele Zahlen zu knacken. Es ist auch ein Erbe des Weltraumzeitalters und des Rüstungswettlaufs und der frühen Versuche, 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 in einer Bank nur Bohnen zählen, funktionieren Softwarelösungen, die in erster Linie Dezimalzeichenfolgen verwenden, einwandfrei. Aber so kann man Quantenchromodynamik oder Aerodynamik nicht machen.

26
DigitalRoss

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, er sich in einen großen fetten Drachen verwandeln und Sie in den A ** beißen wird.

19
workoverflow

Diese seltsamen Zahlen erscheinen, weil Computer zu Berechnungszwecken ein Binärzahlensystem (Basis 2) verwenden, während wir eine Dezimalzahl (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.

16
Piyush S528

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

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

Lassen Sie mich erklären, warum dies die beste Lösung ist. Wie bereits in den obigen Antworten erwähnt, ist es eine gute Idee, die 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 Gleitkommazahlen wie 0.2 und 0.7, hier ist es: 0.2 + 0.7 = 0.8999999999999999.

Ihr erwartetes Ergebnis war 0.9. Dies bedeutet, dass Sie in diesem Fall ein Ergebnis mit einer Genauigkeit von 1 Stelle benötigen. Sie hätten also (0.2 + 0.7).tofixed(1) verwenden sollen, aber Sie können toFix () nicht einfach einen bestimmten Parameter zuweisen, da dies beispielsweise von der angegebenen Zahl abhängt

`0.22 + 0.7 = 0.9199999999999999`

In diesem Beispiel benötigen Sie eine Genauigkeit von 2 Stellen, daher sollte es toFixed(2) sein. Welcher Parameter sollte also für jede gegebene Gleitkommazahl passen?

Man könnte sagen, es sei dann 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 in einer Funktion wie der folgenden anzubieten:

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

Lass es uns selbst versuchen:

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 andeutet, 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 überhaupt nicht funktioniert, obwohl es das gleiche zu sein scheint! Ich bevorzuge die erste Lösung, da ich sie als Funktion anwenden kann, die den Eingabe-Float in einen genauen Ausgabe-Float umwandelt.

15
Mohammad lm71

Viele der zahlreichen Duplikate dieser Frage fragen nach den Auswirkungen der Gleitkommarundung 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 ansieht, die von Interesse sind, als nur darüber zu lesen. Einige Sprachen bieten Möglichkeiten, dies zu tun - beispielsweise das Konvertieren von float oder double in BigDecimal in Java.

Da es sich um eine sprachunabhängige Frage handelt, sind sprachunabhängige Tools wie Decimal to Floating-Point Converter erforderlich.

Wenden Sie es auf die Zahlen in der Frage an, die wie Doppelte behandelt werden:

0.1 konvertiert in 0.1000000000000000055511151231257827021181583404541015625,

0,2 konvertiert in 0,200000000000000011102230246251565404236316680908203125,

0,3 konvertiert in 0,299999999999999988897769753748434595763683319091796875 und

0,30000000000000004 wird in 0,30000000000000004440892098500626169452667236328125 konvertiert.

Wenn Sie die ersten beiden Zahlen manuell oder in einem Dezimalrechner wie Full Precision Calculator hinzufügen, wird die genaue Summe der tatsächlichen Eingaben mit 0,30000000000000000166533453693773481063544750213623046875 angegeben.

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 führt auch zu einem Rundungsfehler von 0,0000000000000000277555756156289135105907917022705078125. Es gilt der Rundum-Gleichstand.

Zurück zum Gleitkommakonverter lautet der rohe Hexadezimalwert für 0,30000000000000004 3fd33333333334, der mit einer geraden Ziffer endet und daher das richtige Ergebnis ist.

14

Da niemand dies erwähnt hat ...

Einige Hochsprachen wie Python und Java enthalten Tools zur Überwindung von Beschränkungen bei binären 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.

    Dezimalzahlen 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 decimal -Modul von Python basiert auf IEEE-Standard 854-1987 .

  • Pythons fractions Modul und Apache Commons BigFraction Klasse . Beide repräsentieren rationale Zahlen als (numerator, denominator) Paare und liefern möglicherweise genauere Ergebnisse als dezimale Gleitkomma-Arithmetik.

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

14

Kann ich nur hinzufügen; Leute gehen immer davon aus, dass dies ein Computerproblem ist, aber wenn Sie mit Ihren Händen zählen (Basis 10), können Sie (1/3+1/3=2/3)=true nur erhalten, wenn Sie unendlich viele 0,333 ... bis 0,333 ... addieren Mit dem Problem (1/10+2/10)!==3/10 in Basis 2 kürzen Sie es auf 0,333 + 0,333 = 0,666 und runden es wahrscheinlich auf 0,667, was auch technisch ungenau wäre.

Zähle ternär, und Drittel sind kein Problem - vielleicht würde ein Rennen mit 15 Fingern pro Hand fragen, warum deine Dezimalrechnung kaputt ist ...

14
user1641172

Die Art von Gleitkomma-Mathematik, die in einem digitalen Computer implementiert werden kann, verwendet notwendigerweise eine Approximation der reellen Zahlen und Operationen auf diesen. (Die Standard -Version umfasst mehr als fünfzig Seiten Dokumentation und hat ein Komitee, das sich mit ihren Errata und ihrer weiteren Verfeinerung befasst.)

Diese Annäherung ist eine Mischung von Annä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 handelt sich auch um 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. mithilfe der Zahl π anstelle einer 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 unscharfen Wert hat 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 drin ist Ihre FPU.

10
Blair Houghton

Aus Spaß habe ich mit der Darstellung von Floats nach den Definitionen des Standards C99 gespielt und den folgenden Code geschrieben.

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

SIGN EXPONENT FRACTION

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

Wenn Sie also float x = 999... schreiben, transformiert der Compiler diese Zahl in eine von der Funktion xx gedruckte Bitdarstellung, sodass die von der Funktion yy gedruckte Summe gleich der angegebenen Zahl ist.

In Wirklichkeit ist diese Summe nur eine Annäherung. Für die Zahl 999.999.999 fügt der Compiler in der Bitdarstellung des Floats die Zahl 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 wirklich in der Hardware vorhanden sind und die vom Compiler dort 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 verwendet, um die Summe der vom Hauptprogramm ausgegebenen Begriffe auszudrucken. Diese Summe kann man 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 mit bc überprüfen, ob -3.14 ebenfalls gestört ist. Vergessen Sie nicht, in scale einen bc -Faktor einzustellen.

Die angezeigte Summe entspricht dem Inhalt der Hardware. Der Wert, den Sie durch Berechnung erhalten, hängt von der von Ihnen eingestellten Skala ab. Ich habe den Faktor scale auf 15 gesetzt. Mathematisch gesehen scheint es mit unendlicher Präzision 1.000.000.000 zu sein.

9
alinsoar

Ein anderer Weg, dies zu betrachten: Verwendet werden 64 Bits, um Zahlen darzustellen. Infolgedessen kann 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 Kodierung, um diese 64 Bit effizient für einen viel größeren Zahlenraum plus NaN und +/- Infinity zu verwenden, sodass es Lücken zwischen genau dargestellten Zahlen gibt, mit denen gefüllt wird Zahlen nur angenähert.

Leider sitzt 0,3 in einer Lücke.

5
Torsten Becker

Da sich dieser Thread ein wenig in eine allgemeine Diskussion über aktuelle Gleitkomma-Implementierungen verzweigte, würde 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 dessen Vorgänger unum) darstellt, der verspricht, eine bessere Genauigkeit mit weniger Bits zu bieten. Wenn mein Verständnis korrekt ist, werden auch die Probleme in der Frage behoben. Ziemlich interessantes Projekt, die Person, die dahinter steht, 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 ).

4
Piedone

Stellen Sie sich vor, Sie arbeiten in der Basis 10 mit einer Genauigkeit von beispielsweise 8 Stellen. Sie prüfen, ob

1/3 + 2 / 3 == 1

und erfahren Sie, dass dies false zurückgibt. Warum? Nun, als reelle Zahlen haben wir

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

Wenn wir auf acht Dezimalstellen kürzen, erhalten wir

0.33333333 + 0.66666666 = 0.99999999

was sich natürlich von 1.00000000 um genau 0.00000001 unterscheidet.


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 zum Beispiel auf sieben Bits kürzen würden, würden wir bekommen

0.0001100 + 0.0011001 = 0.0100101

während auf der anderen Seite,

/10 = 0.01001100110011 ... (Basis 2)

die auf sieben Bits abgeschnitten ist 0.0100110, und diese unterscheiden sich durch genau 0.0000001.


Die genaue Situation ist etwas subtiler, da diese Zahlen typischerweise in wissenschaftlicher Notation gespeichert sind. Anstatt also 1/10 als 0.0001100 zu speichern, können wir es auch als 1.10011 * 2^-4 speichern, je nachdem, wie viele Bits wir dem Exponenten und der Mantisse zugewiesen haben. Dies wirkt sich auf die Anzahl der Stellen aus, die Sie für Ihre Berechnungen erhalten.

Das Fazit ist, dass Sie aufgrund dieser Rundungsfehler im Wesentlichen nie == 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.

3
Daniel McLaury

Seit Python 3.5 können Sie die Funktion math.isclose() verwenden, um die ungefähre Gleichheit zu testen:

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

Es ist eigentlich ziemlich einfach. Wenn Sie ein Basis-10-System haben (wie unseres), 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 ist 1/3, 1/6 und 1/7 sind allesamt sich wiederholende 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, die Enthält nur 2 als Primfaktor. In der Binärdarstellung 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. Also wiederholen 0,1 und 0,2 (1/10 und 1/5), während saubere Dezimalstellen in einem Basis-10-System Dezimalstellen in dem Basis-2-System sind, in dem der Computer arbeitet Diese Werte werden übertragen, wenn Sie die Basis-2-Zahl (Binärzahl) des Computers in eine besser lesbare Basis-10-Zahl konvertieren.

Von https:///0.30000000000000004.com/

2
Vlad Agurets

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;
        }
    },
    diff:{
        value: function(A,B){
            var prec = this.max(this.get_precision(A),this.get_precision(B));
            return +this.precision(A-B,prec);
        }
    },
    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.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false

beachten Sie auch, dass Math.diff und Math.sum die zu verwendende Genauigkeit automatisch erkennen

Math.sum akzeptiert eine beliebige Anzahl von Argumenten

2
bortunac

Dezimalbrüche wie 0.1, 0.2 und 0.3 werden in binär codierten Gleitkommatypen nicht genau dargestellt. Die Summe der Näherungen für 0.1 und 0.2 unterscheidet sich von der für 0.3 verwendeten Näherung, daher die Falschheit von 0.1 + 0.2 == 0.3, wie hier deutlicher zu sehen ist:

#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üssen Sie für Gleitkommawerte eine dezimalbasierte Darstellung verwenden. Der C-Standard spezifiziert solche Typen nicht standardmäßig, sondern als Erweiterung, die in einem Technischen Bericht beschrieben ist. Die Typen _Decimal32, _Decimal64 und _Decimal128 sind möglicherweise auf Ihrem System verfügbar (z. B. gcc unterstützt sie auf ausgewählten Zielen , aber clang unterstützt sie unter OS/X nicht).

0
chqrlie

Eine andere Frage wurde als Duplikat zu dieser Frage benannt:

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

Die x in der Frage ist eine float Variable.

Ein Beispiel wäre

float x = 9.9F;

Der Debugger zeigt 9.89999962, die Ausgabe der cout -Operation ist 9.9.

Die Antwort lautet, dass die Standardgenauigkeit von cout für float 6 ist. Daher wird auf 6 Dezimalstellen gerundet.

Siehe hier als Referenz

0
Arkadiy

Dies war eigentlich als Antwort auf diese Frage gedacht - die als Duplikat von dieser Frage geschlossen wurde, während ich diese Antwort zusammengestellt habe, kann ich sie jetzt nicht dort posten ... also werde ich stattdessen hier posten!


Zusammenfassung der Fragen:

Im Arbeitsblatt werden 10^-8/1000 und 10^-11 als Gleich ausgewertet, während dies in VBA nicht der Fall ist.

In dem Arbeitsblatt sind die Zahlen standardmäßig in wissenschaftlicher Notation.

Wenn Sie die Zellen in ein Zahlenformat ändern (Ctrl+1) von Number mit 15 Dezimalpunkten erhalten Sie:

=10^-11 returns 0.000000000010000
=10^(-8/1000) returns 0.981747943019984

Sie sind also definitiv nicht dasselbe ... einer ist ungefähr null und der andere ungefähr eins.

Excel war nicht dafür ausgelegt, mit extrem kleinen Zahlen umzugehen - zumindest nicht mit der Standardinstallation. Es gibt Add-Ins zur Verbesserung der Genauigkeit von Zahlen.


Excel wurde gemäß dem IEEE-Standard für binäre Gleitkomma-Arithmetik ( IEEE 754 ) entwickelt. Der Standard definiert, wie Gleitkommazahlen gespeichert und berechnet werden. Der Standard IEEE 754 ist weit verbreitet, da er die Speicherung von Gleitkommazahlen auf angemessenem Raum ermöglicht und Berechnungen relativ schnell durchgeführt werden können.

Der Vorteil der Floating-over-Fixed-Point-Darstellung besteht darin, dass ein größerer Wertebereich unterstützt werden kann. Beispielsweise kann eine Festkommadarstellung mit 5 Dezimalstellen und einem Dezimalpunkt nach der dritten Stelle die Zahlen 123.34, 12.23, 2.45 usw. darstellen, während der Gleitkommawert Die Darstellung mit 5-stelliger Genauigkeit kann 1,2345, 12345, 0,00012345 usw. darstellen. In ähnlicher Weise ermöglicht die Gleitkommadarstellung auch Berechnungen über einen weiten Bereich von Größen, während die Genauigkeit beibehalten wird. Zum Beispiel,

img


Weitere Referenzen:

0
ashleedawg