Rozważ następujący kod:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Dlaczego zdarzają się te nieścisłości?
Rozważ następujący kod:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Dlaczego zdarzają się te nieścisłości?
Odpowiedzi:
Binarna matematyka zmiennoprzecinkowa jest taka. W większości języków programowania jest oparty na standardzie IEEE 754 . Sedno problemu polega na tym, że liczby są reprezentowane w tym formacie jako liczba całkowita razy potęga dwóch; liczb wymiernych (takich jak 0.1
, które są 1/10
), których mianownik nie jest potęgą dwóch, nie można dokładnie przedstawić.
W 0.1
standardowym binary64
formacie reprezentację można zapisać dokładnie tak, jak
0.1000000000000000055511151231257827021181583404541015625
dziesiętnie, lub0x1.999999999999ap-4
w notacji heksafloatacyjnej C99 .Natomiast liczba wymierna 0.1
, którą 1/10
można zapisać, może być dokładnie taka jak
0.1
dziesiętnie, lub0x1.99999999999999...p-4
w analogicznym zapisie heksfloatacyjnym C99, gdzie ...
reprezentuje niekończącą się sekwencję 9.Stałe 0.2
i 0.3
w twoim programie będą również przybliżeniami do ich prawdziwych wartości. Zdarza się, że najbliżej double
celu 0.2
jest większa niż liczba racjonalnego 0.2
ale że najbliżej double
celu 0.3
jest mniejsza niż liczba wymierna 0.3
. Suma 0.1
i 0.2
kończy się na wartości większej niż liczba wymierna, 0.3
a zatem nie zgadza się ze stałą w kodzie.
Dość kompleksowe podejście do zagadnień arytmetyki zmiennoprzecinkowej jest tym, co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej . Aby uzyskać łatwiejsze do zrozumienia wyjaśnienie, zobacz floating-point-gui.de .
Uwaga dodatkowa: Wszystkie systemy liczb pozycyjnych (podstawa N) dzielą ten problem z precyzją
Zwykłe stare liczby dziesiętne (podstawa 10) mają te same problemy, dlatego liczby takie jak 1/3 kończą się jako 0.333333333 ...
Właśnie natknąłeś się na liczbę (3/10), która jest łatwa do przedstawienia w systemie dziesiętnym, ale nie pasuje do systemu binarnego. Działa to również w dwie strony (w pewnym stopniu): 1/16 to brzydka liczba dziesiętna (0,0625), ale w systemie dwójkowym wygląda równie porządnie jak 10.000 w systemie dziesiętnym (0,0001) ** - gdybyśmy byli w nawyk korzystania z systemu liczb bazowych 2 w naszym codziennym życiu, można nawet spojrzeć na tę liczbę i instynktownie zrozumieć, że można tam dotrzeć, dzieląc coś o połowę, ponownie o połowę, raz za razem.
** Oczywiście nie tak dokładnie są przechowywane liczby zmiennoprzecinkowe w pamięci (używają one formy notacji naukowej). Jednak ilustruje to, że binarne zmiennoprzecinkowe błędy precyzji mają tendencję do pojawiania się, ponieważ liczby w „świecie rzeczywistym”, z którymi zwykle jesteśmy zainteresowani, to często potęgi dziesięciu - ale tylko dlatego, że używamy dziesiętnego systemu liczb dziesiętnych - dzisiaj. Dlatego też powiemy 71% zamiast „5 na każde 7” (71% to przybliżenie, ponieważ 5/7 nie może być dokładnie przedstawione za pomocą żadnej liczby dziesiętnej).
Więc nie: binarne liczby zmiennoprzecinkowe nie są łamane, po prostu są tak niedoskonałe jak każdy inny system liczb bazowych N :)
Uwaga: Praca z pływakami w programowaniu
W praktyce ten problem precyzji oznacza, że musisz użyć funkcji zaokrąglania, aby zaokrąglić liczby zmiennoprzecinkowe do dowolnej liczby miejsc dziesiętnych, którą jesteś zainteresowany przed ich wyświetleniem.
Trzeba również zastąpić testy równości porównaniami, które dopuszczają pewną tolerancję, co oznacza:
Czy nie robićif (x == y) { ... }
Zamiast zrobić if (abs(x - y) < myToleranceValue) { ... }
.
gdzie abs
jest wartość bezwzględna. myToleranceValue
należy wybrać dla konkretnej aplikacji - i będzie to miało wiele wspólnego z tym, na ile „pokoju wiggle” jesteś gotów pozwolić i jaka może być największa liczba, którą będziesz porównywać (z powodu problemów z precyzją ). Uważaj na stałe w stylu „epsilon” w wybranym języku. Nie należy ich traktować jako wartości tolerancji.
Uważam, że powinienem dodać do tego perspektywę projektanta sprzętu, ponieważ projektuję i buduję sprzęt zmiennoprzecinkowy. Znajomość źródła błędu może pomóc w zrozumieniu tego, co dzieje się w oprogramowaniu, i ostatecznie mam nadzieję, że to pomoże wyjaśnić przyczyny, dla których błędy zmiennoprzecinkowe zdarzają się i wydają się kumulować w miarę upływu czasu.
Z punktu widzenia inżynierii większość operacji zmiennoprzecinkowych będzie zawierała element błędu, ponieważ sprzęt, który wykonuje obliczenia zmiennoprzecinkowe, musi mieć tylko błąd mniejszy niż połowa jednej jednostki na ostatnim miejscu. Dlatego wiele urządzeń zatrzyma się z dokładnością, która jest niezbędna tylko do uzyskania błędu mniejszego niż połowa jednej jednostki w ostatnim miejscu dla pojedynczej operacji, co jest szczególnie problematyczne w przypadku podziału zmiennoprzecinkowego. To, co stanowi pojedynczą operację, zależy od liczby operandów przyjętych przez jednostkę. W większości są to dwa, ale niektóre jednostki przyjmują 3 lub więcej operandów. Z tego powodu nie ma gwarancji, że powtarzające się operacje spowodują pożądany błąd, ponieważ błędy sumują się z czasem.
Większość procesorów jest zgodna ze standardem IEEE-754, ale niektóre wykorzystują normy zdenormalizowane lub inne. Na przykład w IEEE-754 istnieje tryb zdenormalizowany, który pozwala na reprezentację bardzo małych liczb zmiennoprzecinkowych kosztem precyzji. Poniższe informacje dotyczą jednak znormalizowanego trybu IEEE-754, który jest typowym trybem działania.
W standardzie IEEE-754 projektantom sprzętu dozwolona jest dowolna wartość błędu / epsilon, o ile na ostatnim miejscu jest mniejsza niż połowa jednej jednostki, a wynik musi być mniejszy niż połowa jednej jednostki w ostatnim miejsce na jedną operację. To wyjaśnia, dlaczego przy powtarzających się operacjach błędy sumują się. W przypadku podwójnej precyzji IEEE-754 jest to 54-ty bit, ponieważ 53 bity są używane do reprezentowania części liczbowej (znormalizowanej), zwanej także mantysą, liczby zmiennoprzecinkowej (np. 5,3 w 5.3e5). Kolejne sekcje zawierają bardziej szczegółowe informacje na temat przyczyn błędów sprzętowych w różnych operacjach zmiennoprzecinkowych.
Główną przyczyną błędu w dzieleniu zmiennoprzecinkowym są algorytmy podziału stosowane do obliczania ilorazu. Większość systemów komputerowych oblicza dzielenie za pomocą mnożenia przez odwrotność, głównie w Z=X/Y
,Z = X * (1/Y)
. Podział jest obliczany iteracyjnie, tj. Każdy cykl oblicza niektóre bity ilorazu, aż do osiągnięcia pożądanej precyzji, co w przypadku IEEE-754 oznacza wszystko z błędem mniejszym niż jedna jednostka na ostatnim miejscu. Tabela odwrotności Y (1 / Y) jest znana jako tablica wyboru ilorazów (QST) w wolnym podziale, a rozmiar w bitach tabeli selekcji ilorazów jest zwykle szerokością podstawki lub liczby bitów iloraz obliczany w każdej iteracji plus kilka bitów ochronnych. Dla standardu IEEE-754, podwójna precyzja (64-bit), byłby to rozmiar podstawy dzielnika, plus kilka bitów ochronnych k, gdzie k>=2
. Na przykład typową tabelą wyboru ilorazu dla dzielnika, który oblicza 2 bity ilorazu na raz (podstawa 4), byłyby 2+2= 4
bity (plus kilka opcjonalnych bitów).
3.1 Błąd zaokrąglenia podziału: przybliżenie wzajemności
To, co znajduje się w tabeli wyboru ilorazów, zależy od metody podziału : dzielenie wolne, takie jak podział SRT lub szybkie dzielenie, takie jak podział Goldschmidta; każdy wpis jest modyfikowany zgodnie z algorytmem podziału w celu uzyskania możliwie najniższego błędu. W każdym razie wszystkie wzajemności są przybliżeniamirzeczywistej wzajemności i wprowadzić pewien element błędu. Zarówno metody powolnego, jak i szybkiego dzielenia obliczają iloraz iteracyjnie, tj. Pewna liczba bitów ilorazu jest obliczana na każdym etapie, następnie wynik jest odejmowany od dywidendy, a dzielnik powtarza kroki, aż błąd będzie mniejszy niż połowa jednego jednostka na ostatnim miejscu. Metody powolnego dzielenia obliczają stałą liczbę cyfr ilorazu na każdym etapie i zwykle są tańsze w budowie, a metody szybkiego dzielenia obliczają zmienną liczbę cyfr na krok i są zwykle droższe w budowaniu. Najważniejszą częścią metod dzielenia jest to, że większość z nich polega na powtarzanym pomnożeniu przez przybliżenie odwrotności, więc są one podatne na błędy.
Inną przyczyną błędów zaokrąglania we wszystkich operacjach są różne tryby obcinania ostatecznej odpowiedzi, na które zezwala IEEE-754. Są one obcinane, zaokrąglane w kierunku zera, zaokrąglane do najbliższego (domyślnie), zaokrąglane w dół i zaokrąglające w górę. Wszystkie metody wprowadzają element błędu mniejszy niż jedna jednostka w ostatnim miejscu dla pojedynczej operacji. W miarę upływu czasu i powtarzanych operacji obcinanie dodaje się łącznie do powstałego błędu. Ten błąd obcięcia jest szczególnie problematyczny w potęgowaniu, który obejmuje pewną formę powtarzanego mnożenia.
Ponieważ sprzęt, który wykonuje obliczenia zmiennoprzecinkowe, musi tylko dać wynik z błędem mniejszym niż połowa jednej jednostki w ostatnim miejscu dla pojedynczej operacji, błąd będzie narastał w wyniku powtarzanych operacji, jeśli nie będzie obserwowany. Jest to powód, dla którego w obliczeniach wymagających ograniczonego błędu matematycy stosują metody, takie jak użycie zaokrąglenia do najbliższej parzystej cyfry w ostatnim miejscu IEEE-754, ponieważ z czasem błędy częściej się znoszą out i arytmetyka interwałowa w połączeniu z odmianami trybów zaokrąglania IEEE 754przewidywać błędy zaokrąglania i je poprawiać. Z powodu niskiego błędu względnego w porównaniu z innymi trybami zaokrąglania, zaokrąglanie do najbliższej parzystej cyfry (na ostatnim miejscu) jest domyślnym trybem zaokrąglania IEEE-754.
Należy pamiętać, że domyślny tryb zaokrąglania, zaokrąglanie do najbliższej parzystej cyfry w ostatnim miejscu , gwarantuje błąd mniejszy niż połowa jednej jednostki w ostatnim miejscu dla jednej operacji. Samo obcięcie, zaokrąglenie w górę i zaokrąglenie w dół może spowodować błąd, który jest większy niż połowa jednej jednostki na ostatnim miejscu, ale mniejsza niż jedna jednostka na ostatnim miejscu, więc te tryby nie są zalecane, chyba że są używane w arytmetyki interwałowej.
Krótko mówiąc, podstawową przyczyną błędów w operacjach zmiennoprzecinkowych jest kombinacja obcięcia w sprzęcie i obcięcia odwrotności w przypadku podziału. Ponieważ standard IEEE-754 wymaga tylko błędu mniejszego niż połowa jednej jednostki w ostatnim miejscu dla pojedynczej operacji, błędy zmiennoprzecinkowe w przypadku powtarzanych operacji sumują się, o ile nie zostaną poprawione.
Kiedy konwertujesz .1 lub 1/10 na bazę 2 (binarnie), otrzymujesz powtarzający się wzorzec po przecinku, podobnie jak próba przedstawienia 1/3 w podstawie 10. Wartość nie jest dokładna i dlatego nie możesz tego zrobić dokladna matematyka z wykorzystaniem normalnych metod zmiennoprzecinkowych.
Większość odpowiedzi tutaj odnosi się do tego pytania w bardzo suchych, technicznych terminach. Chciałbym rozwiązać ten problem w sposób zrozumiały dla zwykłych ludzi.
Wyobraź sobie, że próbujesz kroić pizze. Masz automatyczny nóż do pizzy, który może pokroić plasterki pizzy dokładnie na pół. Może przekroić całą pizzę o połowę lub może zmniejszyć o połowę istniejący plasterek, ale w każdym razie połówka jest zawsze dokładna.
Ten nóż do pizzy ma bardzo delikatne ruchy, a jeśli zaczynasz od całej pizzy, to przekrój ją na pół i kontynuuj za każdym razem o połowę najmniejszy plasterek, możesz zrobić o połowę 53 razy, zanim plasterek będzie zbyt mały, aby nawet jego precyzyjne zdolności . W tym momencie nie możesz już zmniejszyć o połowę tego bardzo cienkiego plasterka, ale musisz go uwzględnić lub wykluczyć w obecnej postaci.
W jaki sposób poskładałeś wszystkie plasterki w taki sposób, aby tworzyły jedną dziesiątą (0,1) lub jedną piątą (0,2) pizzy? Naprawdę pomyśl o tym i spróbuj to wypracować. Możesz nawet spróbować użyć prawdziwej pizzy, jeśli masz pod ręką mityczny precyzyjny nóż do pizzy. :-)
Oczywiście najbardziej doświadczeni programiści znają prawdziwą odpowiedź, a mianowicie, że nie ma sposobu na ułożenie dokładnie jednej dziesiątej lub piątej pizzy za pomocą tych plasterków, bez względu na to, jak dobrze je pokroisz. Możesz zrobić całkiem dobre przybliżenie, a jeśli dodasz przybliżenie 0,1 do przybliżenia 0,2, otrzymasz całkiem dobre przybliżenie 0,3, ale nadal jest to tylko przybliżenie.
W przypadku liczb o podwójnej precyzji (czyli precyzji, która umożliwia 53-krotne zmniejszenie pizzy), liczby bezpośrednio mniejsze i większe niż 0,1 to 0,09999999999999999167332731531132594682276248931884765625 i 0.1000000000000000055511151231257827021181583404541015625. Ten drugi jest nieco bliższy 0,1 niż poprzedni, więc parser liczbowy, przy danych wejściowych 0,1, faworyzuje ten drugi.
(Różnica między tymi dwiema liczbami to „najmniejszy wycinek”, który musimy zdecydować albo uwzględnić, który wprowadza odchylenie w górę, albo wykluczyć, który wprowadza odchylenie w dół. Terminem technicznym dla tego najmniejszego wycinka jest wrzód ).
W przypadku 0,2 liczby są takie same, po prostu powiększone o współczynnik 2. Ponownie preferujemy wartość nieco wyższą niż 0,2.
Zauważ, że w obu przypadkach przybliżenia dla 0,1 i 0,2 mają nieznaczne odchylenie w górę. Jeśli dodamy wystarczającą liczbę tych uprzedzeń, będą one popychać liczbę coraz dalej od tego, czego chcemy, aw rzeczywistości, w przypadku 0,1 + 0,2, odchylenie jest wystarczająco wysokie, aby wynikowa liczba nie była już najbliższą liczbą do 0,3.
W szczególności, 0,1 + 0,2 jest naprawdę 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, podczas gdy liczba najbliżej 0,3 jest rzeczywiście 0,299999999999999988897769753748434595763683319091796875.
PS Niektóre języki programowania zapewniają również krajalnice do pizzy, które mogą dzielić plasterki na dokładnie jedną dziesiątą . Chociaż takie krajalnice do pizzy są rzadkie, jeśli masz dostęp do jednego, powinieneś go użyć, gdy ważne jest, aby móc uzyskać dokładnie jedną dziesiątą lub jedną piątą plasterka.
Błędy zaokrąglania zmiennoprzecinkowego. 0,1 nie może być reprezentowane tak dokładnie w bazie-2, jak w bazie-10 z powodu brakującego współczynnika liczby pierwszej równej 5. Tak jak 1/3 przyjmuje nieskończoną liczbę cyfr do reprezentacji dziesiętnej, ale wynosi „0,1” w podstawie-3, 0,1 przyjmuje nieskończoną liczbę cyfr w bazie-2, a nie w bazie-10. A komputery nie mają nieskończonej ilości pamięci.
Oprócz innych poprawnych odpowiedzi, możesz rozważyć skalowanie wartości, aby uniknąć problemów z arytmetyką zmiennoprzecinkową.
Na przykład:
var result = 1.0 + 2.0; // result === 3.0 returns true
... zamiast:
var result = 0.1 + 0.2; // result === 0.3 returns false
Wyrażenie 0.1 + 0.2 === 0.3
powraca false
w JavaScript, ale na szczęście arytmetyka liczb całkowitych w liczbach zmiennoprzecinkowych jest dokładna, więc skalowania można uniknąć błędów reprezentacji dziesiętnej.
Jako praktyczny przykład, aby uniknąć problemów z liczbą zmiennoprzecinkową, w których dokładność jest najważniejsza, zaleca się 1 obsługę pieniędzy jako liczby całkowitej reprezentującej liczbę centów: 2550
centów zamiast 25.50
dolarów.
1 Douglas Crockford: JavaScript: Dobre części : Dodatek A - Okropne części (strona 105) .
Moja odpowiedź jest dość długa, dlatego podzieliłem ją na trzy części. Ponieważ pytanie dotyczy matematyki zmiennoprzecinkowej, położyłem nacisk na to, co faktycznie robi maszyna. Uczyniłem go także specyficznym dla podwójnej (64-bitowej) precyzji, ale ten argument stosuje się jednakowo do dowolnej arytmetyki zmiennoprzecinkowej.
Preambuła
IEEE 754 o podwójnej precyzji w formacie binarnym zmiennoprzecinkowych (binary64) liczba oznacza numer formularza
wartość = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023
w 64 bitach:
1
jeśli liczba jest ujemna, w 0
przeciwnym razie 1 .1.
jest zawsze pomijane 2, ponieważ najbardziej znaczącym bitem dowolnej wartości binarnej jest 1
.1 - IEEE 754 pozwala na koncepcję zerowego znaku - +0
i -0
są traktowane inaczej: 1 / (+0)
jest dodatnią nieskończonością; 1 / (-0)
jest ujemną nieskończonością. W przypadku wartości zerowych bity mantysy i wykładnika są równe zero. Uwaga: wartości zerowe (+0 i -0) wyraźnie nie są klasyfikowane jako denormal 2 .
2 - Nie dotyczy to liczb normalnych , które mają wykładnik przesunięcia równy zero (i domniemane 0.
). Zakres denormalnych liczb podwójnej precyzji wynosi d min ≤ | x | ≤ d max , gdzie d min (Najmniejsza ilość niezerowa) wynosi 2 -1023 - 51 (≈ 4,94 * 10 -324 ) i D max (najwięcej Brak reprezentacji, na którym Mantysa składa się całkowicie z 1
S) 2 -1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).
Przekształcanie liczby podwójnej precyzji na binarną
Istnieje wiele konwerterów online do konwersji liczb zmiennoprzecinkowych o podwójnej precyzji na binarne (np. Na binaryconvert.com ), ale oto przykładowy kod C # w celu uzyskania reprezentacji IEEE 754 dla liczby podwójnej precyzji (trzy części oddzielam dwukropkami ( :
) :
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);
}
Przechodząc do sedna: oryginalne pytanie
(Przejdź do dołu dla wersji TL; DR)
Cato Johnston (pytający) zadał pytanie, dlaczego 0,1 + 0,2! = 0,3.
Zapisane w formacie binarnym (z dwukropkami oddzielającymi trzy części) reprezentacje wartości IEEE 754 to:
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010
Zauważ, że mantysa składa się z powtarzających się cyfr 0011
. Jest to kluczem do tego, dlaczego występuje błąd w obliczeniach - 0,1, 0,2 i 0,3 nie mogą być reprezentowane binarnie dokładnie w skończonej liczbie bitów binarnych, a więcej niż 1/9, 1/3 lub 1/7 może być reprezentowane dokładnie w cyfry dziesiętne .
Zauważ też, że możemy zmniejszyć moc wykładnika o 52 i przesunąć punkt w reprezentacji binarnej w prawo o 52 miejsca (podobnie jak 10-3 * 1,23 == 10-5 * 123). To pozwala nam reprezentować reprezentację binarną jako dokładną wartość, którą reprezentuje w postaci * 2 p . gdzie „a” jest liczbą całkowitą.
Konwertowanie wykładników wykładniczych na dziesiętne, usuwanie przesunięcia i ponowne dodawanie domyślnych 1
(w nawiasach kwadratowych), 0,1 i 0,2 to:
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
Aby dodać dwie liczby, wykładnik musi być taki sam, tj .:
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
Ponieważ suma nie ma postaci 2 n * 1. {bbb} zwiększamy wykładnik o jeden i przesuwamy punkt dziesiętny ( binarny ), aby uzyskać:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
W mantysie jest teraz 53 bitów (53. jest w nawiasach kwadratowych w linii powyżej). Domyślny zaokrąglenia tryb IEEE 754 „ okrągły do najbliższego ” - to znaczy czy liczba x mieści się pomiędzy dwoma wartościami a i b , wartości, w której najmniej znaczący bit jest zero, jest wybrany.
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
Zauważ, że i b różnią się tylko w ostatnim bitem; + = . W tym przypadku wartością najmniej znaczącego bitu zero jest b , więc suma wynosi:...0011
1
...0100
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
podczas gdy dwójkowa reprezentacja 0,3 to:
0.3 => 2^-2 * 1.0011001100110011001100110011001100110011001100110011
= 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
która różni się tylko od dwójkowej reprezentacji sumy 0,1 i 0,2 przez 2 -54 .
Binarna reprezentacja 0,1 i 0,2 to najdokładniejsze reprezentacje liczb dopuszczalnych przez IEEE 754. Dodanie tych reprezentacji, ze względu na domyślny tryb zaokrąglania, daje wartość, która różni się tylko bitem najmniej znaczącym.
TL; DR
Pisząc 0.1 + 0.2
w reprezentacji binarnej IEEE 754 (z dwukropkami oddzielającymi trzy części) i porównując ją 0.3
, to jest (umieściłem różne bity w nawiasach kwadratowych):
0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3 => 0:01111111101:0011001100110011001100110011001100110011001100110[011]
Przeliczone z powrotem na dziesiętne, te wartości to:
0.1 + 0.2 => 0.300000000000000044408920985006...
0.3 => 0.299999999999999988897769753748...
Różnica wynosi dokładnie 2 -54 , czyli ~ 5,5511151231258 × 10 -17 - nieistotna (dla wielu aplikacji) w porównaniu z wartościami pierwotnymi.
Porównanie ostatnich bitów liczby zmiennoprzecinkowej jest z natury niebezpieczne, ponieważ każdy, kto czyta słynne „ Co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej ” (który obejmuje wszystkie główne części tej odpowiedzi), będzie wiedział.
Większość kalkulatorów używa dodatkowych cyfr ochronnych, aby obejść ten problem, i tak 0.1 + 0.2
by to dało 0.3
: kilka ostatnich bitów jest zaokrąglanych.
Liczby zmiennoprzecinkowe przechowywane w komputerze składają się z dwóch części, liczby całkowitej i wykładnika, do którego podstawa jest pobierana i mnożona przez część całkowitą.
Gdyby komputer działał w bazie 10, 0.1
byłby 1 x 10⁻¹
, 0.2
byłby 2 x 10⁻¹
i 0.3
byłby 3 x 10⁻¹
. Matematyka liczb całkowitych jest łatwa i dokładna, więc dodawanie 0.1 + 0.2
oczywiście przyniesie skutek 0.3
.
Komputery zwykle nie działają w bazie 10, działają w bazie 2. Nadal możesz uzyskać dokładne wyniki dla niektórych wartości, na przykład 0.5
jest 1 x 2⁻¹
i 0.25
jest 1 x 2⁻²
, i dodając je do 3 x 2⁻²
, lub 0.75
. Dokładnie.
Problem dotyczy liczb, które można przedstawić dokładnie w bazie 10, ale nie w bazie 2. Liczby te należy zaokrąglić do ich najbliższego odpowiednika. Zakładając bardzo popularny 64-bitowy format zmiennoprzecinkowy IEEE, najbliższą liczbą 0.1
jest 3602879701896397 x 2⁻⁵⁵
, a najbliższą liczbą 0.2
jest 7205759403792794 x 2⁻⁵⁵
; zsumowanie ich powoduje 10808639105689191 x 2⁻⁵⁵
uzyskanie dokładnej wartości dziesiętnej lub 0.3000000000000000444089209850062616169452667236328125
. Numery zmiennoprzecinkowe są na ogół zaokrąglane w celu wyświetlenia.
Błąd zaokrąglenia zmiennoprzecinkowego. Z tego, co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej :
Wyciskanie nieskończenie wielu liczb rzeczywistych na skończoną liczbę bitów wymaga przybliżonego przedstawienia. Chociaż istnieje nieskończenie wiele liczb całkowitych, w większości programów wynik obliczeń liczb całkowitych można zapisać w 32 bitach. Z drugiej strony, biorąc pod uwagę dowolną stałą liczbę bitów, większość obliczeń z liczbami rzeczywistymi wytworzy wielkości, których nie da się dokładnie przedstawić za pomocą tylu bitów. Dlatego wynik obliczeń zmiennoprzecinkowych musi często być zaokrąglany, aby dopasować go z powrotem do jego skończonej reprezentacji. Ten błąd zaokrąglenia jest charakterystyczną cechą obliczeń zmiennoprzecinkowych.
Opublikowano wiele dobrych odpowiedzi, ale chciałbym dołączyć jeszcze jedną.
Nie wszystkie numery mogą być reprezentowane przez pływaków / podwaja Na przykład numer „0.2” zostanie przedstawiony jako „0,200000003” w pojedynczej precyzji w standardzie IEEE 754 pkt pływaka.
Model do przechowywania liczb rzeczywistych pod maską reprezentuje liczby zmiennoprzecinkowe jako
Nawet jeśli możesz 0.2
łatwo pisać , FLT_RADIX
a DBL_RADIX
jest to 2; nie 10 dla komputera z FPU, który wykorzystuje „Standard IEEE dla binarnej arytmetyki zmiennoprzecinkowej (ISO / IEEE Std 754–1985)”.
Trudno więc dokładnie przedstawić takie liczby. Nawet jeśli podasz tę zmienną jawnie, bez żadnych pośrednich obliczeń.
Niektóre statystyki związane z tym słynnym pytaniem o podwójnej precyzji.
Przy dodawaniu wszystkich wartości ( a + b ) z krokiem 0,1 (od 0,1 do 100) mamy ~ 15% szansy na błąd precyzji . Zauważ, że błąd może spowodować nieco większe lub mniejsze wartości. Oto kilka przykładów:
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)
Odejmując wszystkie wartości ( a - b, gdzie a> b ), stosując krok 0,1 (od 100 do 0,1), mamy ~ 34% szansy na błąd precyzji . Oto kilka przykładów:
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% i 34% są rzeczywiście ogromne, dlatego zawsze używaj BigDecimal, gdy precyzja ma duże znaczenie. Przy 2 cyfrach dziesiętnych (krok 0,01) sytuacja pogarsza się nieco bardziej (18% i 36%).
Podsumowanie
Arytmetyka zmiennoprzecinkowa jest dokładna, niestety, nie zgadza się dobrze z naszą zwykłą reprezentacją liczby podstawowej-10, więc okazuje się, że często dajemy jej dane wejściowe nieco odbiegające od tego, co napisaliśmy.
Nawet proste liczby, takie jak 0,01, 0,02, 0,03, 0,04 ... 0,24, nie są reprezentowane dokładnie jako ułamki dwójkowe. Jeśli policzysz 0,01, 0,02, 0,03 ..., to dopóki nie dojdziesz do 0,25, otrzymasz pierwszą frakcję reprezentowaną w bazie 2 . Jeśli spróbowałbyś tego za pomocą FP, twój 0,01 byłby nieco mniejszy, więc jedyny sposób na dodanie 25 z nich do dokładnego 0,25 wymagałby długiego łańcucha przyczynowego obejmującego bity ochronne i zaokrąglanie. Trudno przewidzieć, więc podnosimy ręce i mówimy „FP jest niedokładny”, ale to nie jest tak naprawdę prawda.
Stale dajemy sprzętowi FP coś, co wydaje się proste w bazie 10, ale jest powtarzalną frakcją w bazie 2.
Jak to się stało?
Kiedy piszemy w systemie dziesiętnym, każda część (w szczególności każda liczba dziesiętna kończąca się) jest liczbą wymierną postaci
a / (2 n x 5 m )
W trybie binarnym otrzymujemy tylko termin 2 n , to znaczy:
a / 2 n
Więc po przecinku, nie możemy reprezentować 1 / 3 . Ponieważ podstawa 10 zawiera 2 jako czynnik główny, każda liczba, którą możemy zapisać jako ułamek binarny, może być również zapisana jako ułamek podstawowy 10. Jednak prawie wszystko, co piszemy jako ułamek podstawowy 10, jest reprezentowalne w postaci binarnej. W zakresie od 0,01, 0,02, 0,03 ... 0,99 tylko trzy liczby mogą być reprezentowane w naszym formacie FP: 0,25, 0,50 i 0,75, ponieważ są to 1/4, 1/2 i 3/4, wszystkie liczby z czynnikiem podstawowym przy użyciu tylko terminu 2 n .
W podstawie 10 nie może oznaczać 1 / 3 . Ale w binarnym, nie możemy zrobić 1 / 10 lub 1 / 3 .
Tak więc, chociaż każda część binarna może być zapisywana dziesiętnie, odwrotność nie jest prawdą. W rzeczywistości większość ułamków dziesiętnych powtarza się w systemie binarnym.
Radzenie sobie z tym
Programiści są zwykle instruowani, aby robić < porównania epsilon , lepsza rada może być zaokrąglanie do wartości całkowitych (w bibliotece C: round () i roundf (), tj. Pozostanie w formacie FP), a następnie porównanie. Zaokrąglenie do określonej długości ułamka dziesiętnego rozwiązuje większość problemów z wydajnością.
Ponadto w przypadku problemów z liczbami rzeczywistymi (problemy, dla których FP został wymyślony na wczesnych, strasznie drogich komputerach) stałe fizyczne wszechświata i wszystkie inne pomiary są znane tylko stosunkowo niewielkiej liczbie znaczących liczb, więc cała przestrzeń problemu i tak był „niedokładny”. „Dokładność” FP nie stanowi problemu w tego rodzaju aplikacjach.
Cały problem naprawdę powstaje, gdy ludzie próbują wykorzystywać FP do liczenia ziaren. Działa w tym celu, ale tylko wtedy, gdy trzymasz się wartości całkowitych, co w pewnym sensie nie pozwala na ich użycie. Właśnie dlatego mamy te wszystkie biblioteki oprogramowania ułamków dziesiętnych.
Uwielbiam odpowiedź Chrisa autorstwa Chrisa , ponieważ opisuje on rzeczywisty problem, a nie tylko zwykłe wymyślanie „niedokładności”. Gdyby FP były po prostu „niedokładne”, moglibyśmy to naprawić i zrobilibyśmy to dekady temu. Nie zrobiliśmy tego dlatego, że format FP jest kompaktowy i szybki i jest to najlepszy sposób na rozbicie wielu liczb. Jest to również dziedzictwo epoki kosmicznej i wyścigu zbrojeń oraz wczesnych prób rozwiązywania dużych problemów z bardzo powolnymi komputerami używającymi małych systemów pamięci. (Czasami pojedyncze rdzenie magnetyczne do przechowywania 1-bitowego, ale to inna historia. )
Wniosek
Jeśli liczysz tylko fasolę w banku, rozwiązania programowe wykorzystujące w pierwszej kolejności reprezentacje ciągów dziesiętnych działają doskonale. Ale nie można w ten sposób wykonywać kwantowej chromodynamiki ani aerodynamiki.
nextafter()
z przyrostem całkowitym lub dekrementacją binarnej reprezentacji liczb zmiennoprzecinkowych IEEE. Ponadto można porównywać liczby zmiennoprzecinkowe jako liczby całkowite i uzyskać poprawną odpowiedź, z wyjątkiem sytuacji, gdy oba są ujemne (z powodu wielkości znaku w porównaniu z uzupełnieniem 2).
Czy wypróbowałeś taśmę klejącą?
Spróbuj ustalić, kiedy wystąpią błędy i napraw je za pomocą krótkich instrukcji if, jeśli nie są ładne, ale w przypadku niektórych problemów jest to jedyne rozwiązanie i jest to jedno z nich.
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
Miałem ten sam problem w projekcie symulacji naukowej w języku C # i mogę powiedzieć, że jeśli zignorujesz efekt motyla, zmieni się on w dużego, grubego smoka i ugryzie cię w **
Aby zaoferować najlepsze rozwiązanie, które mogę powiedzieć, odkryłem następującą metodę:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
Pozwól mi wyjaśnić, dlaczego jest to najlepsze rozwiązanie. Jak wspomniano w powyższych odpowiedziach, dobrym rozwiązaniem jest skorzystanie z gotowej do użycia funkcji JavaScript toFixed () w celu rozwiązania problemu. Ale najprawdopodobniej napotkasz pewne problemy.
Wyobraź sobie, masz zamiar dodać dwa numery pływaka jak 0.2
i 0.7
to jest tutaj: 0.2 + 0.7 = 0.8999999999999999
.
Oczekiwany wynik oznaczał, 0.9
że w tym przypadku potrzebujesz wyniku z dokładnością do 1 cyfry. Więc powinieneś był użyć(0.2 + 0.7).tofixed(1)
ale nie możesz po prostu podać określonego parametru toFixed (), ponieważ na przykład zależy on od podanej liczby
`0.22 + 0.7 = 0.9199999999999999`
W tym przykładzie potrzebujesz dokładności 2 cyfr, więc powinna być toFixed(2)
, więc jaki powinien być parametr pasujący do każdej podanej liczby zmiennoprzecinkowej?
Można powiedzieć, że w każdej sytuacji będzie to 10:
(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"
Cholera! Co zamierzasz zrobić z tymi niechcianymi zerami po 9? Nadszedł czas, aby przekonwertować go na zmiennoprzecinkowy, aby uzyskać pożądany efekt:
parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9
Teraz, gdy znalazłeś rozwiązanie, lepiej zaoferować je jako funkcję taką jak ta:
function floatify(number){
return parseFloat((number).toFixed(10));
}
Spróbujmy samemu:
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>
Możesz użyć tego w ten sposób:
var x = 0.2 + 0.7;
floatify(x); => Result: 0.9
Jak sugeruje W3SCHOOLS , istnieje również inne rozwiązanie, możesz pomnożyć i podzielić, aby rozwiązać powyższy problem:
var x = (0.2 * 10 + 0.1 * 10) / 10; // x will be 0.3
Pamiętaj, że (0.2 + 0.1) * 10 / 10
to w ogóle nie zadziała, choć wydaje się takie samo! Wolę pierwsze rozwiązanie, ponieważ mogę zastosować je jako funkcję, która przekształca liczbę zmiennoprzecinkową na dokładną liczbę zmiennoprzecinkową.
Te dziwne liczby pojawiają się, ponieważ komputery używają systemu liczb binarnych (podstawa 2) do obliczeń, podczas gdy my używamy liczb dziesiętnych (podstawa 10).
Istnieje większość liczb ułamkowych, których nie można dokładnie przedstawić w postaci binarnej, dziesiętnej ani obu. Wynik - zaokrąglona w górę (ale precyzyjna) liczba wyników.
Wiele z wielu duplikatów tego pytania pyta o wpływ zaokrąglania zmiennoprzecinkowego na określone liczby. W praktyce łatwiej jest zrozumieć, jak to działa, patrząc na dokładne wyniki obliczeń zainteresowania, a nie tylko czytając o tym. Niektóre języki dostarczają sposobów działania, które - jak konwertowania float
lub double
do BigDecimal
w Javie.
Ponieważ jest to pytanie niezależne od języka, potrzebuje narzędzi niezależnych od języka, takich jak konwerter dziesiętny na zmiennoprzecinkowy .
Zastosowanie go do liczb w pytaniu, traktowanych jako podwójne:
0,1 konwertuje na 0.1000000000000000055511151231257827021181583404541015625,
0,2 konwertuje na 0.200000000000000011102230246251565404236316680908203125,
0,3 konwertuje na 0,299999999999999988897769753748434595763683319091796875 i
0.30000000000000004 konwertuje na 0.3000000000000000444089209850062616169452667236328125.
Dodawanie pierwszych dwóch liczb ręcznie lub w kalkulatorze dziesiętnym, takim jak kalkulator pełnej precyzji , pokazuje, że dokładna suma rzeczywistych danych wejściowych wynosi 0,3000000000000000166533453693773481063544750213623046875.
Gdyby zaokrąglić w dół do ekwiwalentu 0,3, błąd zaokrąglenia wyniósłby 0,0000000000000000277555756156289135105907917022705078125. Zaokrąglanie w górę do ekwiwalentu 0,30000000000000004 daje również błąd zaokrąglania 0,000000000000000027275555756156289135105907917022705078125. Obowiązuje równomierny remis zaokrąglający.
Wracając do konwertera zmiennoprzecinkowego, surowy szesnastkowy dla 0.30000000000000004 to 3fd3333333333334, który kończy się na parzystej cyfrze, a zatem jest poprawnym wynikiem.
Biorąc pod uwagę, że nikt nie wspomniał o tym ...
Niektóre języki wysokiego poziomu, takie jak Python i Java, są wyposażone w narzędzia pozwalające pokonać ograniczenia binarne zmiennoprzecinkowe. Na przykład:
decimal
Moduł Pythona i BigDecimal
klasa Java , które wewnętrznie reprezentują liczby z notacją dziesiętną (w przeciwieństwie do notacji binarnej). Oba mają ograniczoną precyzję, więc nadal są podatne na błędy, jednak rozwiązują najczęstsze problemy z binarną arytmetyką zmiennoprzecinkową.
Liczby dziesiętne są bardzo miłe w przypadku pieniędzy: dziesięć centów plus dwadzieścia centów to zawsze dokładnie trzydzieści centów:
>>> 0.1 + 0.2 == 0.3
False
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
decimal
Moduł Pythona oparty jest na standardzie IEEE 854-1987 .
fractions
Moduł Pythona i BigFraction
klasa Apache Common . Obie reprezentują liczby wymierne jako (numerator, denominator)
pary i mogą dawać dokładniejsze wyniki niż dziesiętna arytmetyka zmiennoprzecinkowa.
Żadne z tych rozwiązań nie jest idealne (zwłaszcza jeśli spojrzymy na wyniki lub wymagamy bardzo wysokiej precyzji), ale nadal rozwiązują one wiele problemów z binarną arytmetyką zmiennoprzecinkową.
Czy mogę po prostu dodać; ludzie zawsze zakładają, że jest to problem z komputerem, ale jeśli policzysz rękami (baza 10), nie możesz tego zrobić(1/3+1/3=2/3)=true
dopóki nie masz nieskończoności, aby dodać 0,333 ... do 0,333 ... tak jak w przypadku(1/10+2/10)!==3/10
problemu w bazie 2, obcinasz go do 0,333 + 0,333 = 0,666 i prawdopodobnie zaokrąglasz do 0,667, co również byłoby technicznie niedokładne.
Policz w trójskładniku, a trzecie nie stanowią problemu - być może jakiś wyścig z 15 palcami na każdej ręce zapytałby, dlaczego twoja matematyka dziesiętna została złamana ...
Rodzaj matematyki zmiennoprzecinkowej, który może być zaimplementowany w komputerze cyfrowym, koniecznie wykorzystuje przybliżenie rzeczywistych liczb i operacji na nich. ( Standardowa wersja obejmuje ponad pięćdziesiąt stron dokumentacji i ma komitet zajmujący się jej błędami i dalszymi udoskonaleniami).
To przybliżenie jest mieszanką różnych rodzajów przybliżeń, z których każde może zostać zignorowane lub dokładnie uwzględnione ze względu na specyficzny sposób odchylenia od dokładności. Dotyczy to również wielu wyraźnych wyjątkowych przypadków, zarówno na poziomie sprzętu, jak i oprogramowania, które większość ludzi przechodzi obok, udając, że ich nie zauważa.
Jeśli potrzebujesz nieskończonej precyzji (na przykład używając liczby π zamiast jednego z wielu krótszych stand-ins), powinieneś napisać lub użyć symbolicznego programu matematycznego.
Ale jeśli zgadzasz się z pomysłem, że czasami matematyka zmiennoprzecinkowa jest rozmyta pod względem wartości, a logika i błędy mogą się kumulować szybko, a Ty możesz napisać swoje wymagania i testy, aby to umożliwić, Twój kod często radzi sobie z tym, co jest w środku twój FPU.
Dla zabawy bawiłem się reprezentacją liczb zmiennoprzecinkowych, postępując zgodnie z definicjami standardu C99 i napisałem poniższy kod.
Kod drukuje binarną reprezentację liczb zmiennoprzecinkowych w 3 oddzielnych grupach
SIGN EXPONENT FRACTION
a następnie wypisuje sumę, która, zsumowana z wystarczającą precyzją, pokaże wartość, która naprawdę istnieje w sprzęcie.
Więc kiedy piszesz float x = 999...
, kompilator przekształci tę liczbę w reprezentację bitową wydrukowaną przez funkcję, xx
tak aby suma wydrukowana przez funkcjęyy
była równa podanej liczbie.
W rzeczywistości suma ta jest jedynie przybliżeniem. Dla liczby 999,999,999 kompilator wstawi w reprezentacji bitowej liczby zmiennoprzecinkowej liczbę 1 000 000 000
Po kodzie dołączam sesję konsoli, w której obliczam sumę warunków dla obu stałych (minus PI i 999999999), które naprawdę istnieją w sprzęcie, wstawione tam przez kompilator.
#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);
}
Oto sesja konsoli, w której obliczam rzeczywistą wartość liczby zmiennoprzecinkowej, która istnieje w sprzęcie. Kiedyś bc
drukowałem sumę terminów wyprowadzanych przez program główny. Tę sumę można wstawić do pytona repl
lub czegoś podobnego.
-- .../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
Otóż to. Wartość 999999999 jest w rzeczywistości
999999999.999999446351872
Możesz również sprawdzić, bc
że -3,14 jest również zaburzony. Nie zapomnij ustawić scale
współczynnika bc
.
Wyświetlana suma jest zawarta w sprzęcie. Wartość uzyskana przez obliczenie zależy od ustawionej skali. Ustawiłem scale
współczynnik na 15. Matematycznie, z nieskończoną precyzją, wydaje się, że jest to 1 000 000 000.
Inny sposób spojrzenia na to: używane są 64 bity do reprezentowania liczb. W rezultacie nie można w żaden sposób podać więcej niż 2 ** 64 = 18 446,744,073,709,551,616 różnych liczb.
Jednak matematyka mówi, że istnieje już nieskończenie wiele miejsc po przecinku między 0 a 1. IEE 754 definiuje kodowanie, aby efektywnie wykorzystywać te 64 bity dla znacznie większej liczby cyfr plus NaN i +/- nieskończoność, więc istnieją luki między dokładnie reprezentowanymi liczbami wypełnionymi liczby tylko przybliżone.
Niestety 0,3 pozostaje w przerwie.
Wyobraź sobie pracę w bazie dziesiątej z, powiedzmy, 8 cyframi dokładności. Sprawdzasz czy
1/3 + 2 / 3 == 1
i dowiedz się, że to powraca false
. Dlaczego? Cóż, jako liczby rzeczywiste mamy
1/3 = 0,333 .... i 2/3 = 0,666 ....
Obcinamy z dokładnością do ośmiu miejsc po przecinku
0.33333333 + 0.66666666 = 0.99999999
co oczywiście różni się od 1.00000000
dokładnie 0.00000001
.
Sytuacja dla liczb binarnych ze stałą liczbą bitów jest dokładnie analogiczna. Jako liczby rzeczywiste mamy
1/10 = 0,0001100110011001100 ... (podstawa 2)
i
1/5 = 0,0011001100110011001 ... (podstawa 2)
Jeśli skrócimy je do, powiedzmy, siedmiu bitów, wtedy dostaniemy
0.0001100 + 0.0011001 = 0.0100101
z drugiej strony
3/10 = 0,01001100110011 ... (podstawa 2)
który jest obcięty do siedmiu bitów 0.0100110
i różnią się one dokładnie 0.0000001
.
Dokładna sytuacja jest nieco bardziej subtelna, ponieważ liczby te są zwykle przechowywane w notacji naukowej. Tak więc, na przykład, zamiast przechowywać 1/10, ponieważ 0.0001100
możemy go przechowywać jako coś w rodzaju1.10011 * 2^-4
, w zależności od liczby bitów, które przydzieliliśmy wykładnikowi i mantysie. Wpływa to na liczbę cyfr dokładności uzyskanych w obliczeniach.
Rezultatem jest to, że z powodu tych błędów zaokrąglania zasadniczo nigdy nie chcesz używać == na liczbach zmiennoprzecinkowych. Zamiast tego możesz sprawdzić, czy wartość bezwzględna ich różnicy jest mniejsza niż jakaś stała mała liczba.
Od wersji Python 3.5 możesz używać math.isclose()
funkcji do testowania przybliżonej równości:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
Ponieważ ten wątek nieco rozgałęził się na ogólną dyskusję na temat bieżących implementacji zmiennoprzecinkowych, dodam, że istnieją projekty naprawiania ich problemów.
Spójrz na przykład na https://posithub.org/ , który pokazuje typ liczbowy o nazwie posit (i jego poprzednik unum), który obiecuje zaoferować lepszą dokładność przy mniejszej liczbie bitów. Jeśli moje rozumowanie jest poprawne, rozwiązuje to również rodzaj problemów w pytaniu. Całkiem interesujący projekt, osobą stojącą za nim jest matematyk dr John Gustafson . Całość jest open source, z wieloma rzeczywistymi implementacjami w C / C ++, Python, Julia i C # ( https://hastlayer.com/arithmetics ).
To jest właściwie całkiem proste. Gdy masz system bazowy 10 (taki jak nasz), może on wyrażać tylko ułamki, które wykorzystują czynnik podstawowy bazy. Czynniki pierwsze 10 to 2 i 5. Zatem 1/2, 1/4, 1/5, 1/8 i 1/10 można wyrazić czysto, ponieważ wszystkie mianowniki używają czynników pierwszych 10. Przeciwnie, 1 / 3, 1/6 i 1/7 są powtarzającymi się ułamkami dziesiętnymi, ponieważ ich mianowniki używają współczynnika pierwszego 3 lub 7. W systemie binarnym (lub podstawie 2) jedynym współczynnikiem pierwszym jest 2. Zatem możesz wyrazić tylko ułamki, które zawierają tylko 2 jako główny czynnik. W systemie dwójkowym 1/2, 1/4, 1/8 wszystkie byłyby wyrażone czysto jako miejsca dziesiętne. Podczas gdy 1/5 lub 1/10 powtarzałyby się ułamki dziesiętne. Tak więc 0,1 i 0,2 (1/10 i 1/5), podczas gdy czyste dziesiętne w systemie podstawowym 10, powtarzają dziesiętne w systemie podstawowym 2, w którym działa komputer. Kiedy robisz matematykę na tych powtarzających się dziesiętnych,
Liczby dziesiętne, takie jak 0.1
, 0.2
i 0.3
nie są reprezentowane dokładnie w typach zmiennoprzecinkowych kodowanych binarnie. Suma aproksymacji 0.1
i 0.2
różni się od aproksymacji zastosowanej dla 0.3
, stąd kłamstwo, 0.1 + 0.2 == 0.3
co można lepiej zobaczyć tutaj:
#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;
}
Wynik:
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
Aby obliczenia te były oceniane w bardziej niezawodny sposób, należy użyć reprezentacji dziesiętnej dla wartości zmiennoprzecinkowych. Standard C domyślnie nie określa takich typów, ale jako rozszerzenie opisane w raporcie technicznym .
Te _Decimal32
, _Decimal64
i _Decimal128
typy mogą być dostępne w systemie (na przykład GCC obsługuje je na wybrane cele , ale Clang nie obsługuje ich na OS X ).
Math.sum (javascript) .... rodzaj zamiany operatora
.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001
Object.defineProperties(Math, {
sign: {
value: function (x) {
return x ? x < 0 ? -1 : 1 : 0;
}
},
precision: {
value: function (value, precision, type) {
var v = parseFloat(value),
p = Math.max(precision, 0) || 0,
t = type || 'round';
return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
}
},
scientific_to_num: { // this is from https://gist.github.com/jiggzson
value: function (num) {
//if the number is in scientific notation remove it
if (/e/i.test(num)) {
var zero = '0',
parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
e = parts.pop(), //store the exponential part
l = Math.abs(e), //get the number of zeros
sign = e / l,
coeff_array = parts[0].split('.');
if (sign === -1) {
num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
} else {
var dec = coeff_array[1];
if (dec)
l = l - dec.length;
num = coeff_array.join('') + new Array(l + 1).join(zero);
}
}
return num;
}
}
get_precision: {
value: function (number) {
var arr = Math.scientific_to_num((number + "")).split(".");
return arr[1] ? arr[1].length : 0;
}
},
sum: {
value: function () {
var prec = 0, sum = 0;
for (var i = 0; i < arguments.length; i++) {
prec = this.max(prec, this.get_precision(arguments[i]));
sum += +arguments[i]; // force float to convert strings to number
}
return Math.precision(sum, prec);
}
}
});
Chodzi o to, aby zamiast operatorów liczb zmiennoprzecinkowych używać matematyki
Math.sum automatycznie wykrywa precyzję użycia
Math.sum akceptuje dowolną liczbę argumentów
Rozważ następujące wyniki:
error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1
Widzimy wyraźnie punkt przerwania, kiedy 2**53+1
- wszystko działa dobrze do 2**53
.
>>> (2**53) - int(float(2**53))
0
Dzieje się tak z powodu podwójnej precyzji pliku binarnego: binarny zmiennoprzecinkowy format podwójnej precyzji IEEE 754: binary64
Ze strony Wikipedii dla formatu zmiennoprzecinkowego o podwójnej precyzji :
Binarny zmiennoprzecinkowy podwójnej precyzji jest powszechnie stosowanym formatem na komputerach PC, ze względu na jego szerszy zakres w stosunku do zmiennoprzecinkowego pojedynczej precyzji, pomimo jego wydajności i kosztu przepustowości. Podobnie jak w przypadku formatu zmiennoprzecinkowego o pojedynczej precyzji, brakuje precyzji liczb całkowitych w porównaniu z formatem liczb całkowitych o tym samym rozmiarze. Jest powszechnie znany po prostu jako podwójny. Standard IEEE 754 określa binary64 jako:
- Bit znaku: 1 bit
- Wykładnik: 11 bitów
- Znacząca precyzja: 53 bity (52 jawnie zapisane)
Rzeczywista wartość przyjęta przez dany 64-bitowy układ podwójnej precyzji z danym tendencyjnym wykładnikiem i ułamkiem 52-bitowym wynosi
lub
Dzięki @a_guest za wskazanie mi tego.
Inne pytanie zostało nazwane jako duplikat tego:
W C ++ dlaczego wynik cout << x
różni się od wartości, dla której pokazuje debugger x
?
x
W pytaniu jest float
zmienna.
Przykładem może być
float x = 9.9F;
Debuger pokazuje 9.89999962
, że wynikiem cout
działania jest 9.9
.
Okazuje się, że cout
domyślna precyzja float
wynosi 6, więc zaokrągla do 6 cyfr dziesiętnych.
Zobacz tutaj w celach informacyjnych