Is de drijvende-komma-wiskunde gebroken?

Beschouw de volgende code:

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

Waarom gebeuren deze onnauwkeurigheden?


Antwoord 1, autoriteit 100%

Binaire drijvende kommawiskunde is als volgt. In de meeste programmeertalen is het gebaseerd op de IEEE 754-standaard. De kern van het probleem is dat getallen in dit formaat worden weergegeven als een geheel getal maal een macht van twee; rationale getallen (zoals 0.1, wat 1/10is) waarvan de noemer geen macht van twee is, kunnen niet exact worden weergegeven.

Voor 0.1in het standaard binary64-formaat kan de representatie precies zo geschreven worden

  • 0.1000000000000000055511151231257827021181583404541015625in decimalen, of
  • 0x1.999999999999ap-4in C99 hexfloat-notatie.

Daarentegen kan het rationale getal 0.1, dat 1/10is, precies zo worden geschreven als

  • 0.1in decimaal, of
  • 0x1.99999999999999...p-4in een analoog van C99 hexfloat-notatie, waarbij de ...een oneindige reeks 9-en vertegenwoordigt.

De constanten 0.2en 0.3in uw programma zijn ook benaderingen van hun werkelijke waarden. Het komt voor dat het doubledat het dichtst bij 0.2ligt, groter is dan het rationale getal 0.2maar dat het doublehet dichtst bij 0.3is kleiner dan het rationale getal 0.3. De som van 0.1en 0.2wordt uiteindelijk groter dan het rationale getal 0.3en is het dus niet eens met de constante in je code.

Een redelijk uitgebreide behandeling van problemen met drijvende komma’s is Wat elke computerwetenschapper moet weten over rekenkunde met drijvende komma. Voor een gemakkelijker te begrijpen uitleg, zie floating-point-gui.de.

Kanttekening: alle positionele (grondtal-N) nummersystemen delen dit probleem met precisie

Gewone oude decimale (grondtal 10) getallen hebben dezelfde problemen, daarom eindigen getallen als 1/3 op 0,3333333333…

Je bent zojuist een getal (3/10) tegengekomen dat gemakkelijk te representeren is met het decimale stelsel, maar niet in het binaire stelsel past. Het gaat ook beide kanten op (tot op zekere hoogte): 1/16 is een lelijk getal in decimaal (0,0625), maar in binair getal ziet het er net zo netjes uit als een 10.000e in decimaal (0,0001)** – als we in de gewoonte om in ons dagelijks leven een getalsysteem met grondtal 2 te gebruiken, zou je zelfs naar dat getal kijken en instinctief begrijpen dat je daar zou kunnen komen door iets te halveren, het opnieuw te halveren, en opnieuw en opnieuw.

** Dat is natuurlijk niet precies hoe getallen met drijvende komma in het geheugen worden opgeslagen (ze gebruiken een vorm van wetenschappelijke notatie). Het illustreert echter wel het punt dat binaire precisiefouten met drijvende komma de neiging hebben om op te duiken omdat de ‘echte wereld’-getallen waarmee we gewoonlijk willen werken zo vaak machten van tien zijn – maar alleen omdat we een decimaal getalsysteem gebruiken. vandaag. Dit is ook de reden waarom we dingen zeggen als 71% in plaats van “5 van de 7” (71% is een benadering, aangezien 5/7 niet exact kan worden weergegeven met een decimaal getal).

Dus nee: binaire getallen met drijvende komma zijn niet gebroken, ze zijn toevallig net zo onvolmaakt als elk ander basis-N-getalsysteem 🙂

Opmerking aan de zijkant: werken met floats in programmeren

In de praktijk betekent dit precisieprobleem dat u afrondingsfuncties moet gebruiken om uw getallen met drijvende komma af te ronden op het aantal decimalen waarin u geïnteresseerd bent voordat u ze weergeeft.

U moet ook gelijkheidstests vervangen door vergelijkingen die enige tolerantie toestaan, wat betekent:

Doe nietdoe if (x == y) { ... }

Doe in plaats daarvan if (abs(x - y) < myToleranceValue) { ... }.

waarbij absde absolute waarde is. myToleranceValuemoet worden gekozen voor uw specifieke toepassing – en het heeft veel te maken met hoeveel “bewegingsruimte” u bereid bent toe te staan, en wat het grootste aantal is dat u gaat vergelijken worden (vanwege verlies van precisie). Pas op voor “epsilon” stijlconstanten in uw taal naar keuze. Deze mogen nietworden gebruikt als tolerantiewaarden.


Antwoord 2, autoriteit 25%

Het perspectief van een hardwareontwerper

Ik denk dat ik hier het perspectief van een hardware-ontwerper aan moet toevoegen, aangezien ik drijvende-komma-hardware ontwerp en bouw. Het kennen van de oorsprong van de fout kan helpen om te begrijpen wat er in de software gebeurt, en uiteindelijk hoop ik dat dit helpt om de redenen te verklaren waarom drijvende-kommafouten optreden en zich in de loop van de tijd lijken op te stapelen.

1. Overzicht

Vanuit een technisch perspectief zullen de meeste drijvende-kommabewerkingen een element van fouten bevatten, aangezien de hardware die de drijvende-kommaberekeningen uitvoert slechts een fout van minder dan de helft van een eenheid hoeft te hebben. Daarom stopt veel hardware met een precisie die alleen nodig is om een fout van minder dan de helft van een eenheid op te leveren, in de laatste plaats voor een enkele bewerking, wat vooral problematisch is bij deling van drijvende komma’s. Wat een enkele bewerking is, hangt af van het aantal operanden dat de eenheid nodig heeft. Voor de meesten zijn het er twee, maar sommige eenheden hebben 3 of meer operanden. Daarom is er geen garantie dat herhaalde bewerkingen resulteren in een gewenste fout, aangezien de fouten in de loop van de tijd oplopen.

2. Normen

De meeste processors volgen de IEEE-754-standaard, maar sommige gebruiken gedenormaliseerde of andere standaarden
. Er is bijvoorbeeld een gedenormaliseerde modus in IEEE-754 die weergave van zeer kleine getallen met drijvende komma mogelijk maakt ten koste van de precisie. Het volgende zal echter betrekking hebben op de genormaliseerde modus van IEEE-754, wat de typische werkingsmodus is.

In de IEEE-754-standaard mogen hardware-ontwerpers elke waarde van fout/epsilon gebruiken, zolang deze op de laatste plaats minder dan de helft van een eenheid is, en het resultaat slechts minder dan de helft van één eenheid hoeft te zijn in de laatste plaats voor één operatie. Dit verklaart waarom bij herhaalde bewerkingen de fouten optellen. Voor IEEE-754 dubbele precisie is dit de 54e bit, aangezien 53 bits worden gebruikt om het numerieke deel (genormaliseerd), ook wel de mantisse genoemd, van het drijvende-kommagetal weer te geven (bijvoorbeeld de 5.3 in 5.3e5). De volgende secties gaan dieper in op de oorzaken van hardwarefouten bij verschillende drijvende-kommabewerkingen.

3. Oorzaak van afrondingsfout in deling

De belangrijkste oorzaak van de fout bij de deling met drijvende komma zijn de delingsalgoritmen die worden gebruikt om het quotiënt te berekenen. De meeste computersystemen berekenen deling door middel van vermenigvuldiging met een inverse, voornamelijk in Z=X/Y, Z = X * (1/Y). Een deling wordt iteratief berekend, d.w.z. elke cyclus berekent enkele bits van het quotiënt totdat de gewenste precisie is bereikt, wat voor IEEE-754 alles is met een fout van minder dan één eenheid op de laatste plaats. De tabel van reciprocals van Y (1/Y) staat bekend als de quotiëntselectietabel (QST) in de langzame deling, en de grootte in bits van de quotiëntselectietabel is meestal de breedte van de radix, of een aantal bits van het in elke iteratie berekende quotiënt, plus een paar bewakingsbits. Voor de IEEE-754-standaard, dubbele precisie (64-bit), zou dit de grootte van de radix van de deler zijn, plus een paar bewakingsbits k, waarbij k>=2. Dus bijvoorbeeld, een typische quotiëntselectietabel voor een deler die 2 bits van het quotiënt tegelijk berekent (radix 4) zou 2+2= 4bits zijn (plus een paar optionele bits).

3.1 Afrondingsfout van deling: benadering van wederzijdse

Welke reciprocals in de quotiëntselectietabel staan, hangt af van de verdelingsmethode: langzame divisie zoals SRT divisie, of snelle divisie zoals Goldschmidt divisie; elke invoer wordt aangepast volgens het delingsalgoritme in een poging om de laagst mogelijke fout op te leveren. Hoe dan ook, alle reciprocals zijn benaderingenvan het eigenlijke reciproke en introduceren een element van fout. Zowel langzame als snelle delingsmethoden berekenen het quotiënt iteratief, dat wil zeggen dat elke stap een aantal bits van het quotiënt wordt berekend, vervolgens wordt het resultaat afgetrokken van het deeltal en herhaalt de deler de stappen totdat de fout kleiner is dan de helft van één eenheid op de laatste plaats. Langzame delingsmethoden berekenen een vast aantal cijfers van het quotiënt in elke stap en zijn meestal minder duur om te bouwen, en snelle delingsmethoden berekenen een variabel aantal cijfers per stap en zijn meestal duurder om te bouwen. Het belangrijkste onderdeel van de delingsmethoden is dat de meeste gebaseerd zijn op herhaalde vermenigvuldiging met een benaderingvan een reciproke, dus zijn ze vatbaar voor fouten.

4. Afrondingsfouten in andere bewerkingen: afkapping

Een andere oorzaak van de afrondingsfouten in alle bewerkingen zijn de verschillende manieren van afkappen van het uiteindelijke antwoord dat IEEE-754 toestaat. Er is truncate, round-towards-zero, round-to-nearest (standaard),round – naar beneden en naar boven. Alle methoden introduceren een foutelement van minder dan één eenheid in de laatste plaats voor een enkele bewerking. In de loop van de tijd en bij herhaalde bewerkingen, voegt afkapping ook cumulatief toe aan de resulterende fout. Deze afbreekfout is vooral problematisch bij machtsverheffing, waarbij een of andere vorm van herhaalde vermenigvuldiging betrokken is.

5. Herhaalde bewerkingen

Aangezien de hardware die de drijvende-kommaberekeningen uitvoert, slechts een resultaat hoeft te leveren met een fout van minder dan de helft van een eenheid in de laatste plaats voor een enkele bewerking, zal de fout bij herhaalde bewerkingen toenemen als er niet naar wordt gekeken. Dit is de reden dat wiskundigen in berekeningen die een begrensde fout vereisen, methoden gebruiken zoals het gebruik van de round-to-nearest zelfs een cijfer op de laatste plaatsvan IEEE-754, omdat na verloop van tijd de fouten elkaar eerder opheffen, en Interval Arithmeticgecombineerd met variaties van de IEEE 754 afrondingsmodiom afrondingsfouten te voorspellen en te corrigeren. Vanwege de lage relatieve fout in vergelijking met andere afrondingsmodi, is afronden naar het dichtstbijzijnde even cijfer (in de laatste plaats), de standaard afrondingsmodus van IEEE-754.

Merk op dat de standaard afrondingsmodus, round-to-nearest zelfs cijfers op de laatste plaats, garandeert een fout van minder dan de helft van een eenheid in de laatste plaats voor één bewerking. Het gebruik van alleen de afkapping, afronding en afronding naar beneden kan resulteren in een fout die groter is dan de helft van een eenheid op de laatste plaats, maar minder dan één eenheid op de laatste plaats, dus deze modi worden niet aanbevolen tenzij ze zijn gebruikt in intervalrekenkunde.

6. Samenvatting

Kortom, de fundamentele reden voor de fouten in drijvende-kommabewerkingen is een combinatie van de truncatie in hardware en de truncatie van een reciproke in het geval van deling. Aangezien de IEEE-754-standaard slechts een fout van minder dan de helft van één eenheid vereist voor een enkele bewerking, zullen de drijvende-kommafouten over herhaalde bewerkingen optellen, tenzij gecorrigeerd.


Antwoord 3, autoriteit 20%

Het is op precies dezelfde manier gebroken als de decimale (grondtal-10) notatie die je op de lagere school hebt geleerd, is gebroken, alleen voor grondtal-2.

Als u dit wilt begrijpen, kunt u overwegen 1/3 als een decimale waarde weer te geven. Het is onmogelijk om precies te doen! Op dezelfde manier kan 1/10 (decimaal 0,1) niet exact in grondtal 2 (binair) worden weergegeven als een “decimale” waarde; een herhalend patroon achter de komma gaat voor altijd door. De waarde is niet exact en daarom kun je er geen exacte wiskunde mee doen met behulp van normale drijvende-kommamethoden.


Antwoord 4, autoriteit 9%

Floating-point afrondingsfouten. 0.1 kan niet zo nauwkeurig worden weergegeven in grondtal-2 als in grondtal-10 vanwege de ontbrekende priemfactor van 5. Net zoals 1/3 een oneindig aantal cijfers nodig heeft om in decimaal weer te geven, maar “0,1” is in grondtal-3, 0.1 heeft een oneindig aantal cijfers in basis-2, waar het niet in basis-10 is. En computers hebben geen oneindige hoeveelheid geheugen.


Antwoord 5, autoriteit 5%

Naast de andere juiste antwoorden, kunt u overwegen uw waarden te schalen om problemen met drijvende-kommaberekeningen te voorkomen.

Bijvoorbeeld:

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

… in plaats van:

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

De uitdrukking 0.1 + 0.2 === 0.3retourneert falsein JavaScript, maar gelukkig is het rekenen met gehele getallen in floating-point exact, dus fouten in decimale weergave kunnen worden vermeden door schalen.

Als praktisch voorbeeld, om problemen met drijvende komma’s te vermijden waarbij nauwkeurigheid van het grootste belang is, wordt het aanbevolen1om geld te verwerken als een geheel getal dat het aantal centen vertegenwoordigt: 2550cent in plaats van 25.50dollar.


1Douglas Crockford: JavaScript: de goede delen: Bijlage A – Verschrikkelijke onderdelen (pagina 105).


Antwoord 6, autoriteit 5%

Mijn antwoord is vrij lang, dus ik heb het in drie delen opgesplitst. Aangezien de vraag over drijvende-komma-wiskunde gaat, heb ik de nadruk gelegd op wat de machine eigenlijk doet. Ik heb het ook specifiek gemaakt voor dubbele (64-bits) precisie, maar het argument is evenzeer van toepassing op alle drijvende-kommaberekeningen.

Preambule

Een IEEE 754 binaire floating-point-indeling met dubbele precisie (binary64)getal staat voor een getal van de vorm

waarde = (-1)^s * (1.m51m50…m2m1 m0)2* 2e-1023

in 64 bits:

  • Het eerste bit is het tekenbit: 1als de getal is negatief, 0anders1.
  • De volgende 11 bits zijn de exponent, dat is offsetmet 1023. Met andere woorden, na het lezen van de exponentbits van een getal met dubbele precisie, moet 1023 worden afgetrokken om de macht van twee.
  • De resterende 52 bits zijn de significand(of mantisse). In de mantisse wordt een ‘impliciete’ 1.altijd2weggelaten omdat het meest significante bit van een binaire waarde 1is.

1– IEEE 754 staat het concept toe van een ondertekende nul+0en -0worden verschillend behandeld: 1 / (+0)is positief oneindig; 1 / (-0)is negatief oneindig. Voor nulwaarden zijn de mantisse- en exponentbits allemaal nul. Opmerking: nulwaarden (+0 en -0) worden expliciet niet geclassificeerd als denormal2.

2– Dit is niet het geval voor denormale getallen, die een offset-exponent van nul hebben (en een impliciete 0.). Het bereik van denormale dubbele precisiegetallen is dmin≤ |x| ≤ dmax, waarbij dmin(het kleinste getal dat niet nul is) 2-1023 – 51is (≈ 4.94 * 10– 324) en dmax(het grootste denormale getal, waarvoor de mantisse volledig uit 1s bestaat) is 2-1023 + 1– 2-1023 – 51(≈ 2.225 * 10-308).


Een getal met dubbele precisie omzetten in binair

Er bestaan veel online converters om een drijvende-kommagetal met dubbele precisie naar binair te converteren (bijvoorbeeld op binaryconvert.com), maar hier is een voorbeeld van een C#-code om de IEEE 754-representatie voor een getal met dubbele precisie te verkrijgen (ik scheid de drie delen met dubbele punten (:):

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);
}

Naar het punt: de oorspronkelijke vraag

(Skip naar de onderkant voor de TL; DR-versie)

Cato Johnston (de vraag Askeur) gevraagd waarom 0,1 + 0.2! = 0,3.

Geschreven in binair (met dubbele punten die de drie delen scheiden), zijn de IEEE 754-representaties van de waarden:

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

Merk op dat de Mantissa is samengesteld uit terugkerende cijfers van 0011. Dit is -toets op waarom er een fout is op de berekeningen – 0,1, 0.2 en 0,3 kan niet worden weergegeven in binair precies in een finite aantal Binaire bits van meer dan 1/9, 1/3 of 1/7 kan nauwkeurig worden vertegenwoordigd in decimale cijfers .

Houd er ook rekening mee dat we het vermogen in de exponent met 52 kunnen verlagen en het punt in de binaire weergave naar rechts vóór 52 plaatsen verschuiven (net als 10 -3 * 1.23 == 10 -5 * 123). Hierdoor kunnen we de binaire weergave vertegenwoordigen als de exacte waarde die het vertegenwoordigt in het formulier A * 2 P . waar ‘A’ een geheel getal is.

De exponenten omzetten naar decimaal, het verwijderen van de offset en het opnieuw toevoegen van de impliciete 1(in vierkante beugels), 0,1 en 0,2 zijn:

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

Om twee cijfers toe te voegen, moet de exponent hetzelfde zijn, d.w.z.:

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

Aangezien de som niet van de vorm 2 is N * 1. {BBB} We verhogen de exponent met één en verschuiven de decimale (Binary ) punt om te krijgen:

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

Er zijn nu 53 bits in de Mantissa (de 53e is in vierkante haakjes in de bovenstaande lijn). De standaard afrondingsmodus voor IEEE 754 is ‘rond naar dichtstbijzijnde ‘- dwz als een cijfer x valt tussen twee waarden A en B , de waarde waar het minst significante bit nul is gekozen.

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

Merk op dat aen balleen in het laatste bit verschillen; ...0011+ 1= ...0100. In dit geval is de waarde met het minst significante bit nul b, dus de som is:

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

terwijl de binaire weergave van 0,3 is:

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

die alleen verschilt van de binaire weergave van de som van 0,1 en 0,2 met 2-54.

De binaire representatie van 0.1 en 0.2 zijn de meest nauwkeurigerepresentaties van de getallen die zijn toegestaan door IEEE 754. De toevoeging van deze representaties, vanwege de standaard afrondingsmodus, resulteert in een waarde die alleen verschilt in de minst significante bit.

TL;DR

Het schrijven van 0.1 + 0.2in een IEEE 754 binaire representatie (met dubbele punten die de drie delen scheiden) en het vergelijken met 0.3, dit is (ik heb de verschillende bits tussen vierkante haken):

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

Teruggeconverteerd naar decimaal, deze waarden zijn:

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

Het verschil is precies 2 -54 , dat ~ 5.5511151231258 × 10 -17 – onbeduidende (voor veel toepassingen) is in vergelijking met de oorspronkelijke waarden.

Het vergelijken van de laatste paar stukjes van een drijvend puntnummer is inherent gevaarlijk, zoals iedereen die de beroemde “Wat elke computerwetenschapper moet weten over rekenkundige drijvende rekenkunde ” (die alle belangrijke delen van dit antwoord omvat) zal het weten.

De meeste rekenmachines gebruiken extra guard cijfers om dit probleem rond te komen, wat is hoe 0.1 + 0.2zou 0.3geven: de laatste paar bits zijn afgerond.


Antwoord 7, autoriteit 2%

Floating-point afrondingsfout. Van Wat elke computerwetenschapper moet weten over drijvende-komma-rekenkunde:

Het samenpersen van oneindig veel reële getallen in een eindig aantal bits vereist een benadering bij benadering. Hoewel er oneindig veel gehele getallen zijn, kan in de meeste programma’s het resultaat van gehele berekeningen in 32 bits worden opgeslagen. Daarentegen, gegeven een vast aantal bits, zullen de meeste berekeningen met reële getallen hoeveelheden opleveren die niet exact kunnen worden weergegeven met zoveel bits. Daarom moet het resultaat van een drijvende-kommaberekening vaak worden afgerond om weer in de eindige weergave te passen. Deze afrondingsfout is het karakteristieke kenmerk van drijvende-kommaberekening.


8

Mijn oplossing:

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

Precision verwijst naar het aantal cijfers dat u wilt behouden na het decimale punt tijdens de toevoeging.


9

Er zijn veel goede antwoorden gepost, maar ik wil er nog één toevoegen.

Niet alle getallen kunnen worden weergegeven via floats/doubles
Het getal “0.2” wordt bijvoorbeeld weergegeven als “0.200000003” in enkele precisie in de IEEE754 floatpoint-standaard.

Model voor echte winkelgetallen onder de motorkap vertegenwoordigen vlottergetallen als

Hoewel je 0.2gemakkelijk kunt typen, is FLT_RADIXen DBL_RADIX2; not 10 voor een computer met FPU die gebruikmaakt van “IEEE Standard for Binary Floating-Point Arithmetic (ISO/IEEE Std 754-1985)”.

Dus het is een beetje moeilijk om zulke getallen precies weer te geven. Zelfs als u deze variabele expliciet opgeeft zonder enige tussenberekening.


Antwoord 10

Nee, niet gebroken, maar de meeste decimale breuken moeten worden benaderd

Samenvatting

Drijvende-kommaberekening isexact, helaas komt het niet goed overeen met onze gebruikelijke representatie van basis-10-getallen, dus het blijkt dat we het vaak invoer geven die enigszins afwijkt van wat we schreven.

Zelfs eenvoudige getallen zoals 0,01, 0,02, 0,03, 0,04 … 0,24 kunnen niet exact als binaire breuken worden weergegeven. Als u 0.01, .02, .03 … optelt, krijgt u pas bij 0.25 de eerste breuk die representatief is in grondtal2. Als je dat had geprobeerd met FP, zou je 0,01 iets afwijken, dus de enige manier om er 25 op te tellen tot een mooie exacte 0,25 zou een lange keten van causaliteit vereisen met bewakingsbits en afronding. Het is moeilijk te voorspellen, dus we steken onze handen in de lucht en zeggen “FP is inexact”,maar dat is niet echt waar.

We geven de FP-hardware constant iets dat eenvoudig lijkt in basis 10, maar een herhalende breuk is in basis 2.

Hoe is dit gebeurd?

Als we in decimalen schrijven, is elke breuk (in het bijzonder elke eindpunt decimaal)een rationaal getal van de vorm

          
a / (2nx 5m)

In binair krijgen we alleen de term 2n, dat wil zeggen:

           a / 2n

Dus in decimalen kunnen we 1/3niet weergeven. Omdat grondtal 10 2 als priemfactor bevat, kan elk getal dat we als binaire breuk kunnen schrijven ookworden geschreven als breuk met grondtal 10. Bijna alles wat we schrijven als een basis10breuk is echter binair weer te geven. In het bereik van 0,01, 0,02, 0,03 … 0,99 kunnen slechts driegetallen worden weergegeven in ons FP-formaat: 0,25, 0,50 en 0,75, omdat ze 1/4, 1/2, en 3/4, alle getallen met een priemfactor die alleen de term 2ngebruiken.

In basis10kunnen we 1/3niet vertegenwoordigen. Maar in binair getal kunnen we geen 1/10of1/3.

Dus hoewel elke binaire breuk in decimalen kan worden geschreven, is het omgekeerde niet waar. En in feite worden de meeste decimale breuken binair herhaald.

Omgaan met het

Ontwikkelaars krijgen meestal de opdracht om < epsilonvergelijkingen, zou een beter advies kunnen zijn om af te ronden op integrale waarden (in de C-bibliotheek: round() en roundf(), d.w.z. blijf in het FP-formaat) en vergelijk dan. Afronding naar een specifieke decimale breuklengte lost de meeste problemen met uitvoer op.

Ook bij echte rekenproblemen (de problemen waarvoor FP is uitgevonden op vroege, vreselijk dure computers) zijn de fysieke constanten van het universum en alle andere metingen slechts bekend bij een relatief klein aantal significante cijfers, dus de de hele probleemruimte was sowieso “onnauwkeurig”. FP “nauwkeurigheid” is geen probleem in dit soort toepassingen.

Het hele probleem ontstaat wanneer mensen FP proberen te gebruiken voor het tellen van bonen. Daar werkt het wel voor, maar alleen als je je aan integrale waarden houdt, wat het nut van het gebruik ervan teniet doet. Daarom hebben we al die softwarebibliotheken voor decimale breuken.

Ik ben dol op het pizza-antwoord van Chris, omdat het het eigenlijke probleem beschrijft, niet alleen het gebruikelijke handzwaaien over “onnauwkeurigheid”. Als FP gewoon “onnauwkeurig” zou zijn, hadden we dat kunnen reparerenen zouden we dat tientallen jaren geleden hebben gedaan. De reden dat we dat niet hebben gedaan, is omdat het FP-formaat compact en snel is en het de beste manier is om veel getallen te verwerken. Het is ook een erfenis uit het ruimtetijdperk en de wapenwedloop en vroege pogingen om grote problemen op te lossen met zeer trage computers met kleine geheugensystemen. (Soms individuele magnetische kernenvoor 1-bits opslag, maar dat is een ander verhaal .)

Conclusie

Als je gewoon bonen aan het tellen bent bij een bank, werken softwareoplossingen die in de eerste plaats decimale tekenreeksen gebruiken perfect. Maar op die manier kun je geen kwantumchromodynamica of aerodynamica doen.


Antwoord 11

Enkele statistieken met betrekking tot deze beroemde vraag met dubbele precisie.

Als we alle waarden (a + b) optellen met een stap van 0,1 (van 0,1 tot 100), hebben we ~15% kans op precisiefouten. Houd er rekening mee dat de fout kan resulteren in iets grotere of kleinere waarden.
Hier zijn enkele voorbeelden:

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)

Bij het aftrekken van alle waarden (a – bwaarbij a > b) met een stap van 0,1 (van 100 tot 0,1), hebben we ~34% kans op precisiefout.
Hier zijn enkele voorbeelden:

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% en 34% zijn inderdaad enorm, dus gebruik altijd BigDecimal als precisie van groot belang is. Met 2 cijfers achter de komma (stap 0.01) wordt de situatie nog iets erger (18% en 36%).


Antwoord 12

Heb je de ducttape-oplossing geprobeerd?

Probeer te bepalen wanneer fouten optreden en repareer ze met korte if-statements, het is niet mooi, maar voor sommige problemen is dit de enige oplossing en dit is er een van.

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

Ik had hetzelfde probleem in een wetenschappelijk simulatieproject in c#, en ik kan je vertellen dat als je het vlindereffect negeert, het in een dikke draak verandert en je in de a** bijt.


Antwoord 13

Die rare getallen verschijnen omdat computers een binair (grondtal 2) getalsysteem gebruiken voor berekeningsdoeleinden, terwijl wij decimale (grondtal 10) gebruiken.

Er is een meerderheid van fractionele getallen die niet exact kunnen worden weergegeven in binair of decimaal of beide. Resultaat – Een naar boven afgerond (maar nauwkeurig) resultaat.


Antwoord 14

Aangezien niemand dit heeft vermeld…

Sommige talen op hoog niveau, zoals Python en Java, worden geleverd met tools om binaire drijvende-kommabeperkingen te omzeilen. Bijvoorbeeld:

  • Python’s decimal-moduleen Java’s BigDecimal-klasse, die intern getallen vertegenwoordigen met decimale notatie (in tegenstelling tot binaire notatie). Beide hebben een beperkte precisie, dus ze zijn nog steeds foutgevoelig, maar ze lossen de meest voorkomende problemen met binaire drijvende-kommaberekeningen op.

    Decimalen zijn erg leuk bij het omgaan met geld: tien cent plus twintig cent is altijd precies dertig cent:

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

    Python’s decimal-module is gebaseerd op IEEE-standaard 854-1987.

  • Python’s fractionsmodule en Apache Common’s BigFractionKlasse . Beide vertegenwoordigen rationele nummers als (numerator, denominator)paren en ze kunnen meer accuraat resultaten geven dan decimale drijvende punt rekenkunde.

Geen van deze oplossingen is perfect (vooral als we naar uitvoeringen kijken, of als we een zeer hoge precisie nodig hebben), maar toch oplossen ze een groot aantal problemen met binair drijvend punt rekenkundig.


15

Veel van deze vraag’s vele duplicaten vragen over de effecten van drijvend punt afronding op specifieke nummers. In de praktijk is het gemakkelijker om een ​​gevoel te krijgen voor hoe het werkt door te kijken naar de exacte resultaten van berekeningen in plaats van in plaats van er gewoon over te lezen. Sommige talen bieden manieren om dat te doen – zoals het converteren van een floatof doublenaar BigDecimalin Java.

Aangezien dit een taal-agnostische vraag is, heeft het taal-agnostische gereedschappen nodig, zoals een Decimaal tot zwendelpuntconvertor .

het toepassen op de cijfers in de vraag, behandeld als dubbelspel:

0.1 converteert naar 0.100000000000000000055511151231257827021181583404541015625,

0.2 Converteert naar 0.20000000000000000011102230246251565404236316680908203125,

0,3 converteert naar 0.29999999999999998889776975374843459597683319091796875 en

0.30000000000000004 Converteert naar 0.3000000000000000444089209850062616169452667236328125.

De eerste twee getallen handmatig toevoegen of in een decimale rekenmachine zoals Full Precision Calculator, laat zien dat de exacte som van de werkelijke invoer 0,3000000000000000166533453693773481063544750213623046875 is.

Als het naar beneden zou worden afgerond op het equivalent van 0,3, zou de afrondingsfout 0.0000000000000000277555756156289135105907917022705078125 zijn. Afronding naar het equivalent van 0.30000000000000004 geeft ook een afrondingsfout 0.00000000000000002775555756156289135105907917022705078125. De round-to-even tie-breaker is van toepassing.

Terugkerend naar de drijvende-kommaconverter: de ruwe hexadecimale waarde voor 0,30000000000000004 is 3fd33333333333334, wat eindigt op een even cijfer en daarom het juiste resultaat is.


Antwoord 16

Mag ik even toevoegen; mensen gaan er altijd van uit dat dit een computerprobleem is, maar als je met je handen telt (basis 10), krijg je (1/3+1/3=2/3)=trueniet tenzij je hebt oneindig om 0,333… tot 0,333… toe te voegen, dus net als bij het (1/10+2/10)!==3/10probleem in grondtal 2, kap je het af tot 0,333 + 0,333 = 0,666 en rond het waarschijnlijk af op 0,667, wat ook technisch onnauwkeurig zou zijn.

Tellen in ternair, en tertsen is echter geen probleem – misschien zou een race met 15 vingers aan elke hand vragen waarom je decimale wiskunde was gebroken…


Antwoord 17

Het soort drijvende-komma-wiskunde dat in een digitale computer kan worden geïmplementeerd, gebruikt noodzakelijkerwijs een benadering van de reële getallen en bewerkingen daarop. (De standaardversie beslaat meer dan vijftig pagina’s aan documentatie en heeft een commissie om de fouten en verdere verfijning ervan af te handelen.)

Deze benadering is een mengeling van benaderingen van verschillende soorten, die elk ofwel kunnen worden genegeerd of zorgvuldig kunnen worden verklaard vanwege de specifieke manier van afwijking van de nauwkeurigheid. Het gaat ook om een aantal expliciete uitzonderlijke gevallen op zowel hardware- als softwareniveau waar de meeste mensen langs lopen terwijl ze doen alsof ze het niet merken.

Als je oneindige precisie nodig hebt (bijvoorbeeld door het getal π te gebruiken in plaats van een van de vele kortere stand-ins), moet je in plaats daarvan een symbolisch wiskundeprogramma schrijven of gebruiken.

Maar als je het eens bent met het idee dat drijvende-komma-wiskunde soms vaag in waarde is en logica en fouten zich snel kunnen ophopen, en je kunt je vereisten en tests schrijven om dat mogelijk te maken, dan kan je code vaak rondkomen met wat er in uw FPU zit.


Antwoord 18

Voor de lol speelde ik met de weergave van drijvers, volgens de definities van de Standard C99 en schreef ik de onderstaande code.

De code drukt de binaire weergave van drijvers af in 3 gescheiden groepen

SIGN EXPONENT FRACTION

en daarna drukt het een som af, die, wanneer deze met voldoende precisie wordt opgeteld, de waarde laat zien die echt bestaat in hardware.

Dus als je float x = 999...schrijft, zal de compiler dat getal transformeren in een bitrepresentatie die wordt afgedrukt door de functie xxzodat de som die wordt afgedrukt door de functie yymoet gelijk zijn aan het opgegeven getal.

In werkelijkheid is dit bedrag slechts een benadering. Voor het getal 999.999.999 zal de compiler in de bitweergave van de float het getal 1.000.000.000 invoegen

Na de code voeg ik een consolesessie toe, waarin ik de som van termen bereken voor beide constanten (min PI en 999999999) die echt in hardware bestaat, daar ingevoegd door de compiler.

#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 is een consolesessie waarin ik de reële waarde van de float bereken die bestaat in hardware. Ik heb bcgebruikt om de bedrag van de termen af ​​te drukken die door het hoofdprogramma zijn uitgevoerd. Men kan die som invoegen in Python replof iets dergelijks ook.

-- .../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

Dat is het. De waarde van 999999999 is in feite

999999999.999999446351872

Je kunt ook met bccontroleren of -3.14 ook gestoord is. Vergeet niet een scalefactor in te stellen in bc.

De weergegeven som is wat er in de hardware zit. De waarde die u verkrijgt door deze te berekenen, hangt af van de schaal die u instelt. Ik heb de scale-factor op 15 gezet. Wiskundig, met oneindige precisie, lijkt het 1.000.000.000 te zijn.


Antwoord 19

Een andere manier om hier naar te kijken: gebruikt worden 64 bits om getallen weer te geven. Als gevolg hiervan kunnen er niet meer dan 2**64 = 18.446.744.073.709.551.616 verschillende getallen nauwkeurig worden weergegeven.

Math zegt echter dat er al oneindig veel decimalen tussen 0 en 1 zijn. IEE 754 definieert een codering om deze 64 bits efficiënt te gebruiken voor een veel grotere getalruimte plus NaN en +/- Infinity, dus er zijn hiaten tussen nauwkeurig weergegeven getallen gevuld met getallen alleen bij benadering.

Helaas zit 0,3 in een gat.


Antwoord 20

Sinds Python 3.5kun je math.isclose()functie voor het testen van gelijkheid bij benadering:

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

Antwoord 21

Stel je voor dat je in basis tien werkt met, laten we zeggen, 8 cijfers nauwkeurig. Je controleert of

1/3 + 2 / 3 == 1

en leer dat dit falseretourneert. Waarom? Nou, als echte getallen hebben we

1/3 = 0,333….en 2/3 = 0,666….

Afkappen op acht decimalen, krijgen we

0.33333333 + 0.66666666 = 0.99999999

wat natuurlijk precies 0.00000001verschilt van 0.00000001.


De situatie voor binaire getallen met een vast aantal bits is precies analoog. Als reële getallen hebben we

1/10 = 0.0001100110011001100… (grondtal 2)

en

1/5 = 0,0011001100110011001… (grondtal 2)

Als we deze zouden afkappen tot bijvoorbeeld zeven bits, dan krijgen we

0.0001100 + 0.0011001 = 0.0100101

terwijl aan de andere kant,

3/10 = 0,01001100110011… (grondtal 2)

wat, afgekort tot zeven bits, 0.0100110is, en deze verschillen precies 0.0000001.


De exacte situatie is iets subtieler omdat deze getallen doorgaans in wetenschappelijke notatie worden opgeslagen. Dus in plaats van 1/10 op te slaan als 0.0001100, kunnen we het bijvoorbeeld opslaan als 1.10011 * 2^-4, afhankelijk van hoeveel bits we hebben toegewezen voor de exponent en de mantisse. Dit is van invloed op het aantal nauwkeurige cijfers dat u krijgt voor uw berekeningen.

Het resultaat is dat je vanwege deze afrondingsfouten in principe nooit == op getallen met drijvende komma wilt gebruiken. In plaats daarvan kun je controleren of de absolute waarde van hun verschil kleiner is dan een vast klein getal.


Antwoord 22

Decimale getallen zoals 0.1, 0.2en 0.3worden niet exact weergegeven in binair gecodeerde typen met drijvende komma. De som van de benaderingen voor 0.1en 0.2verschilt van de benadering die wordt gebruikt voor 0.3, vandaar de onwaarheid van 0.1 + 0.2 == 0.3zoals hier duidelijker te zien is:

#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;
}

Uitvoer:

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

Om deze berekeningen betrouwbaarder te kunnen evalueren, moet u een op decimalen gebaseerde weergave gebruiken voor drijvende-kommawaarden. De C-standaard specificeert dergelijke typen standaard niet, maar als een uitbreiding beschreven in een technisch rapport.

De typen _Decimal32, _Decimal64en _Decimal128zijn mogelijk beschikbaar op uw systeem (bijvoorbeeld GCCondersteunt ze op geselecteerde doelen, maar Clanga>ondersteunt ze niet op OS X).


Antwoord 23

Drijvende-kommagetallen worden op hardwareniveau weergegeven als fracties van binaire getallen (grondtal 2). Bijvoorbeeld de decimale breuk:

0.125

heeft de waarde 1/10 + 2/100 + 5/1000 en, op dezelfde manier, de binaire breuk:

0.001

heeft de waarde 0/2 + 0/4 + 1/8. Deze twee breuken hebben dezelfde waarde, het enige verschil is dat de eerste een decimale breuk is, de tweede een binaire breuk.

Helaas kunnen de meeste decimale breuken niet exact worden weergegeven in binaire breuken. Daarom worden de drijvende-kommagetallen die u geeft in het algemeen alleen benaderd door binaire breuken die in de machine moeten worden opgeslagen.

Het probleem is gemakkelijker te benaderen in grondtal 10. Neem bijvoorbeeld de breuk 1/3. Je kunt het benaderen tot een decimale breuk:

0.3

of beter,

0.33

of beter,

0.333

enz. Hoeveel decimalen je ook schrijft, het resultaat is nooit precies 1/3, maar het is een schatting die altijd dichterbij komt.

Op dezelfde manier kan de decimale waarde 0.1 niet exact worden weergegeven als een binaire breuk, ongeacht hoeveel decimalen met grondtal 2 u gebruikt. In grondtal 2 is 1/10 het volgende periodieke getal:

0.0001100110011001100110011001100110011001100110011 ...

Stop bij een willekeurig eindig aantal bits en je krijgt een benadering.

Voor Python worden op een typische machine 53 bits gebruikt voor de precisie van een float, dus de waarde die wordt opgeslagen wanneer u de decimale 0,1 invoert, is de binaire breuk.

0.00011001100110011001100110011001100110011001100110011010

wat dicht, maar niet precies gelijk is aan 1/10.

Het is gemakkelijk om te vergeten dat de opgeslagen waarde een benadering is van de oorspronkelijke decimale breuk, vanwege de manier waarop floats worden weergegeven in de interpreter. Python geeft alleen een decimale benadering weer van de waarde die in binair is opgeslagen. Als Python de werkelijke decimale waarde van de binaire benadering die is opgeslagen voor 0.1 zou uitvoeren, zou het het volgende opleveren:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Dit zijn veel meer decimalen dan de meeste mensen zouden verwachten, dus Python geeft een afgeronde waarde weer om de leesbaarheid te verbeteren:

>>> 0.1
0.1

Het is belangrijk om te begrijpen dat dit in werkelijkheid een illusie is: de opgeslagen waarde is niet precies 1/10, het is gewoon op het display dat de opgeslagen waarde wordt afgerond. Dit wordt duidelijk zodra u rekenkundige bewerkingen uitvoert met deze waarden:

>>> 0.1 + 0.2
0.30000000000000004

Dit gedrag is inherent aan de aard van de drijvende-kommaweergave van de machine: het is geen fout in Python, noch een fout in uw code. U kunt hetzelfde soort gedrag waarnemen in alle andere talen die hardware-ondersteuning gebruiken voor het berekenen van drijvende-kommagetallen (hoewel sommige talen het verschil niet standaard zichtbaar maken, of niet in alle weergavemodi).

Een andere verrassing is inherent aan deze. Als u bijvoorbeeld de waarde 2.675 probeert af te ronden op twee decimalen, krijgt u

>>> round (2.675, 2)
2.67

De documentatie voor de primitieve round() geeft aan dat deze naar de dichtstbijzijnde waarde vanaf nul afrondt. Aangezien de decimale breuk precies halverwege tussen 2,67 en 2,68 ligt, zou je (een binaire benadering van) 2,68 moeten verwachten. Dit is echter niet het geval, omdat wanneer de decimale breuk 2.675 wordt omgezet in een float, deze wordt opgeslagen door een benadering waarvan de exacte waarde is:

2.67499999999999982236431605997495353221893310546875

Omdat de benadering iets dichter bij 2,67 dan bij 2,68 ligt, is de afronding naar beneden.

Als u zich in een situatie bevindt waarin het belangrijk is om decimale getallen half naar beneden af ​​te ronden, moet u de decimale module gebruiken. Trouwens, de decimale module biedt ook een handige manier om de exacte waarde te “zien” die is opgeslagen voor elke float.

>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')

Een ander gevolg van het feit dat 0.1 niet precies in 1/10 wordt opgeslagen, is dat de som van tien waarden van 0.1 ook geen 1.0 geeft:

>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999

De rekenkunde van binaire getallen met drijvende komma houdt veel van dergelijke verrassingen in. Het probleem met “0.1” wordt hieronder in detail uitgelegd, in de sectie “Representatiefouten”. Zie The Perils of Floating Point voor een completere lijst van dergelijke verrassingen.

Het is waar dat er geen eenvoudig antwoord is, maar wees niet al te wantrouwend tegenover zwevende virtula-getallen! Fouten, in Python, in drijvende-kommabewerkingen zijn te wijten aan de onderliggende hardware, en op de meeste machines zijn niet meer dan 1 op 2 ** 53 per bewerking. Dit is meer dan nodig voor de meeste taken, maar u moet er rekening mee houden dat dit geen decimale bewerkingen zijn en dat elke bewerking op drijvende-kommagetallen een nieuwe fout kan veroorzaken.

Hoewel er pathologische gevallen bestaan, krijgt u voor de meest voorkomende gebruiksgevallen het verwachte resultaat aan het eind door eenvoudig naar boven af te ronden op het aantal decimalen dat u op het scherm wilt. Voor nauwkeurige controle over hoe floats worden weergegeven, zie Syntaxis voor stringopmaak voor de opmaakspecificaties van de str.format ()-methode.

Dit deel van het antwoord legt in detail het voorbeeld van “0.1” uit en laat zien hoe u zelf een exacte analyse van dit soort zaken kunt uitvoeren. We gaan ervan uit dat u bekend bent met de binaire weergave van getallen met drijvende komma. De term Weergavefout betekent dat de meeste decimale breuken niet exact binair kunnen worden weergegeven. Dit is de belangrijkste reden waarom Python (of Perl, C, C++, Java, Fortran en vele anderen) meestal niet het exacte resultaat in decimalen weergeeft:

>>> 0.1 + 0.2
0.30000000000000004

Waarom? 1/10 en 2/10 zijn niet exact in binaire breuken weer te geven. Alle machines van vandaag (juli 2010) volgen echter de IEEE-754-standaard voor het rekenen van drijvende-kommagetallen. en de meeste platforms gebruiken een “IEEE-754 dubbele precisie” om Python-drijvers weer te geven. Dubbele precisie IEEE-754 gebruikt 53 bits precisie, dus bij het lezen probeert de computer 0.1 om te zetten naar de dichtstbijzijnde fractie van de vorm J / 2 ** N met J een geheel getal van precies 53 bits. Herschrijven:

1/10 ~ = J / (2 ** N)

in :

J ~ = 2 ** N / 10

onthoud dat J precies 53 bits is (so> = 2 ** 52 maar <2 ** 53), de best mogelijke waarde voor N is 56:

>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793

Dus 56 is de enige mogelijke waarde voor N die precies 53 bits overlaat voor J. De best mogelijke waarde voor J is daarom dit quotiënt, afgerond:

>>> q, r = divmod (2 ** 56, 10)
>>> r
6

Aangezien de carry groter is dan de helft van 10, wordt de beste benadering verkregen door naar boven af te ronden:

>>> q + 1
7205759403792794

Daarom is de best mogelijke benadering voor 1/10 in “IEEE-754 dubbele precisie” deze boven 2 ** 56, dat wil zeggen:

7205759403792794/72057594037927936

Merk op dat aangezien de afronding naar boven is gedaan, het resultaat eigenlijk iets groter is dan 1/10; als we niet naar boven hadden afgerond, zou het quotiënt iets minder dan 1/10 zijn geweest. Maar in geen geval is het precies 1/10!

Dus de computer “ziet” nooit 1/10: wat hij ziet is de exacte breuk die hierboven is gegeven, de beste benadering met behulp van de drijvende-kommagetallen met dubbele precisie van de “” IEEE-754 “:

>>>. 1 * 2 ** 56
7205759403792794.0

Als we deze breuk vermenigvuldigen met 10 ** 30, kunnen we de waarden van de 30 decimalen met een sterk gewicht waarnemen.

>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L

wat betekent dat de exacte waarde die in de computer is opgeslagen ongeveer gelijk is aan de decimale waarde 0,10000000000000000005551115123125. In versies voorafgaand aan Python 2.7 en Python 3.1 rondde Python deze waarden af ​​op 17 significante decimalen, waarbij “0.1000000000000001” werd weergegeven. In de huidige versies van Python is de weergegeven waarde de waarde waarvan de breuk zo kort mogelijk is, terwijl exact dezelfde weergave wordt gegeven wanneer deze wordt teruggeconverteerd naar binair, door simpelweg “0.1” weer te geven.


Antwoord 24

Aangezien deze thread een beetje vertakt is in een algemene discussie over de huidige drijvende-komma-implementaties, zou ik willen toevoegen dat er projecten zijn om hun problemen op te lossen.

Kijk bijvoorbeeld eens naar https://posithub.org/, waar een nummertype met de naam posit wordt getoond (en zijn voorganger unum) die belooft een betere nauwkeurigheid te bieden met minder bits. Als ik het goed begrijp, lost het ook het soort problemen in de vraag op. Best interessant project, de persoon erachter is een wiskundige, het Dr. John Gustafson. Het geheel is open source, met veel daadwerkelijke implementaties in C/C++, Python, Julia en C# (https://hastlayer. com/rekenkunde).


Antwoord 25

Het is eigenlijk vrij eenvoudig. Als je een systeem met grondtal 10 hebt (zoals het onze), kan het alleen breuken uitdrukken die een priemfactor van het grondtal gebruiken. De priemfactoren van 10 zijn 2 en 5. Dus 1/2, 1/4, 1/5, 1/8 en 1/10 kunnen allemaal netjes worden uitgedrukt omdat de noemers allemaal priemfactoren van 10 gebruiken. Daarentegen is 1 /3, 1/6 en 1/7 zijn allemaal herhalende decimalen omdat hun noemers een priemfactor van 3 of 7 gebruiken. In binair (of grondtal 2) is de enige priemfactor 2. Dus je kunt alleen breuken netjes uitdrukken die bevatten slechts 2 als priemfactor. In binair zouden 1/2, 1/4, 1/8 allemaal netjes worden uitgedrukt als decimalen. Terwijl 1/5 of 1/10 herhalende decimalen zijn. Dus 0,1 en 0,2 (1/10 en 1/5), terwijl schone decimalen in een systeem met basis 10, herhalende decimalen zijn in het systeem met basis 2 waarin de computer werkt. Als je rekent met deze herhalende decimalen, krijg je restjes die worden overgedragen wanneer u het (binaire) getal met grondtal 2 van de computer omzet in een voor mensen leesbaar getal met grondtal 10.

Van https://0.30000000000000004.com/


Antwoord 26

Ik zag zojuist dit interessante probleem rond zwevende punten:

Beschouw de volgende resultaten:

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

We kunnen duidelijk een breekpunt zien wanneer 2**53+1– alles werkt prima tot 2**53.

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

Dit gebeurt vanwege de dubbele-precisie binaire: IEEE 754 dubbele-precisie binaire drijvende-komma-indeling: binary64

Van de Wikipedia-pagina voor Double-precision floating-point-formaat:

Dubbele precisie binaire drijvende komma is een veelgebruikte indeling op pc’s, vanwege het grotere bereik ten opzichte van enkele precisie drijvende komma, ondanks de prestaties en bandbreedtekosten. Net als bij een enkelvoudig-precisie floating-point-formaat, mist het precisie op gehele getallen in vergelijking met een integer-formaat van dezelfde grootte. Het is algemeen bekend als dubbel. De IEEE 754-standaard specificeert een binary64 met:

  • Tekenbit: 1 bit
  • Exponent: 11 bits
  • Aanzienlijke precisie: 53 bits (52 expliciet opgeslagen)

De werkelijke waarde die wordt aangenomen door een gegeven 64-bits dubbele-precisiedatum met een gegeven vooringenomen exponent en een 52-bits breuk is

of

Bedankt aan @a_guest dat je me erop hebt gewezen.


Antwoord 27

Normale rekenkunde is grondtal 10, dus decimalen stellen tienden, honderdsten, enz. voor. Als je een getal met drijvende komma probeert weer te geven in binaire rekenkunde met grondtal 2, heb je te maken met helften, kwarten, achtsten, enz.

p>

In de hardware worden drijvende punten opgeslagen als integere mantissen en exponenten. Mantisse vertegenwoordigt de significante cijfers. Exponent is als wetenschappelijke notatie, maar het gebruikt een basis van 2 in plaats van 10. 64.0 zou bijvoorbeeld worden weergegeven met een mantisse van 1 en exponent van 6. 0,125 zou worden weergegeven met een mantisse van 1 en een exponent van -3.

Drijvende komma decimalen moeten negatieve machten van 2 optellen

0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d

en ga zo maar door.

Het is gebruikelijk om een error-delta te gebruiken in plaats van gelijkheidsoperatoren bij het omgaan met drijvende-kommaberekeningen. In plaats van

if(a==b) ...

u zou gebruiken

delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...

Other episodes