Czy matematyka zmiennoprzecinkowa jest zepsuta?


2981

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?


127
Zmienne zmiennoprzecinkowe zwykle mają takie zachowanie. Jest to spowodowane tym, jak są przechowywane w sprzęcie. Aby uzyskać więcej informacji, zobacz artykuł w Wikipedii na temat liczb zmiennoprzecinkowych .
Ben S

62
JavaScript traktuje dziesiętne jako liczby zmiennoprzecinkowe , co oznacza, że ​​operacje takie jak dodawanie mogą podlegać błędowi zaokrąglania. Warto przyjrzeć się temu artykułowi: Co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej
mat b

4
Tylko dla informacji, WSZYSTKIE typy liczbowe w javascript są podwójnymi IEEE-754.
Gary Willoughby,

6
Ponieważ JavaScript używa standardu IEEE 754 dla matematyki, wykorzystuje 64-bitowe liczby zmiennoprzecinkowe. Powoduje to błędy precyzji podczas wykonywania obliczeń zmiennoprzecinkowych (dziesiętnych), krótko mówiąc, z powodu komputerów pracujących w bazie 2, podczas gdy liczba dziesiętna to baza 10 .
Pardeep Jain

Odpowiedzi:


2249

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.1standardowym binary64formacie reprezentację można zapisać dokładnie tak, jak

Natomiast liczba wymierna 0.1, którą 1/10można zapisać, może być dokładnie taka jak

  • 0.1 dziesiętnie, lub
  • 0x1.99999999999999...p-4w analogicznym zapisie heksfloatacyjnym C99, gdzie ...reprezentuje niekończącą się sekwencję 9.

Stałe 0.2i 0.3w twoim programie będą również przybliżeniami do ich prawdziwych wartości. Zdarza się, że najbliżej doublecelu 0.2jest większa niż liczba racjonalnego 0.2ale że najbliżej doublecelu 0.3jest mniejsza niż liczba wymierna 0.3. Suma 0.1i 0.2kończy się na wartości większej niż liczba wymierna, 0.3a 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 absjest wartość bezwzględna. myToleranceValuenależ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.


181
Myślę, że „jakaś stała błędu” jest bardziej poprawna niż „Epsilon”, ponieważ nie ma „Epsilon”, którego można by użyć we wszystkich przypadkach. W różnych sytuacjach należy używać różnych epsilonów. Maszyna epsilon prawie nigdy nie jest dobrym stałym narzędziem.
Rotsor

34
Nie do końca jest prawdą, że cała matematyka zmiennoprzecinkowa oparta jest na standardzie IEEE [754]. Na przykład nadal stosuje się niektóre systemy, które mają stary szesnastkowy układ FP IBM, i nadal istnieją karty graficzne, które nie obsługują arytmetyki IEEE-754. Jest to jednak zgodne z rozsądnym przybliżeniem.
Stephen Canon

19
Cray porzucił zgodność z IEEE-754 dla prędkości. Java poluzowała również przestrzeganie zasad optymalizacji.
Art Taylor,

28
Myślę, że powinieneś dodać coś do tej odpowiedzi na temat tego, w jaki sposób obliczenia na pieniądzach zawsze powinny być zawsze wykonywane za pomocą arytmetyki stałoprzecinkowej na liczbach całkowitych , ponieważ pieniądze są kwantowane. (Może mieć sens wykonywanie wewnętrznych obliczeń księgowych w małych ułamkach centa lub jakiejkolwiek najmniejszej jednostce walutowej - często pomaga to np. Zmniejszyć błąd zaokrąglenia przy przeliczaniu „29,99 $ miesięcznie” na stawkę dzienną - ale powinien wciąż być arytmetyką stałoprzecinkową.)
zwol

18
Interesujący fakt: ten 0,1 nie jest dokładnie reprezentowany w binarnym zmiennoprzecinkowym, co spowodowało niesławny błąd oprogramowania rakietowego Patriot, który spowodował śmierć 28 osób podczas pierwszej wojny w Iraku.
hdl

602

Perspektywa projektanta sprzętu

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.

1. Przegląd

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.

2. Normy

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.

3. Przyczyna błędu zaokrąglania w podziale

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= 4bity (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.

4. Błędy zaokrąglania w innych operacjach: Obcinanie

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.

5. Powtarzane operacje

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.

6. Podsumowanie

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.


8
(3) jest zły. Błąd zaokrąglania w podziale nie jest mniejszy niż jedna jednostka na ostatnim miejscu, ale co najwyżej połowa jednostki na ostatnim miejscu.
gnasher729

6
@ gnasher729 Good catch. W większości podstawowych operacji występuje także błąd mniejszy niż 1/2 jednej jednostki w ostatnim miejscu przy użyciu domyślnego trybu zaokrąglania IEEE. Edytowałem wyjaśnienie, a także zauważyłem, że błąd może być większy niż 1/2 z jednej ulp, ale mniej niż 1 ulp, jeśli użytkownik zastąpi domyślny tryb zaokrąglania (jest to szczególnie prawdziwe w systemach osadzonych).
KernelPanik

39
(1) zmiennoprzecinkowe numery nie mają błąd. Każda wartość zmiennoprzecinkowa jest dokładnie taka, jaka jest. Większość (ale nie wszystkie) operacji zmiennoprzecinkowych daje niedokładne wyniki. Na przykład nie ma binarnej wartości zmiennoprzecinkowej, która byłaby dokładnie równa 1,0 / 10,0. Niektóre operacje (np, 1,0 + 1,0) należy podać dokładne wyniki z drugiej strony.
Solomon Slow

19
„Główną przyczyną błędu w dzieleniu zmiennoprzecinkowym są algorytmy podziału stosowane do obliczania ilorazu” - to bardzo mylące stwierdzenie. W przypadku podziału zgodnego z IEEE-754 jedyną przyczyną błędu w dzieleniu zmiennoprzecinkowym jest niemożność dokładnego przedstawienia wyniku w formacie wyniku; ten sam wynik jest obliczany niezależnie od zastosowanego algorytmu.
Stephen Canon

6
@Matt Przepraszamy za opóźnienie w odpowiedzi. Jest to zasadniczo spowodowane problemami z zasobami / czasem i kompromisami. Istnieje sposób na podział długi / bardziej „normalny”, nazywa się SRT Division z podstawką drugą. Jednak to wielokrotnie przesuwa i odejmuje dzielnik od dywidendy i zajmuje wiele cykli zegara, ponieważ oblicza tylko jeden bit ilorazu na cykl zegara. Używamy tabel wzajemności, abyśmy mogli obliczyć więcej bitów ilorazu na cykl i dokonać efektywnych kompromisów wydajności / prędkości.
KernelPanik

462

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.


133
Świetna i krótka odpowiedź. Powtarzający się wzór wygląda jak 0.00011001100110011001100110011001100110011001100110011 ...
Konstantin Chernov

4
To nie wyjaśnia, dlaczego nie jest używany lepszy algorytm, który nie przekształca się w pliki binarne.
Dmitri Zaitsev

12
Ponieważ wydajność. Korzystanie z plików binarnych jest kilka tysięcy razy szybsze, ponieważ jest natywne dla maszyny.
Joel Coehoorn

7
Istnieją metody, które dają dokładne wartości dziesiętne. BCD (Binary Decimal Decimal) lub różne inne formy liczb dziesiętnych. Jednak oba są wolniejsze (dużo wolniejsze) i zajmują więcej miejsca niż użycie binarnego zmiennoprzecinkowego. (na przykład spakowany BCD przechowuje 2 cyfry dziesiętne w bajcie. To 100 możliwych wartości w bajcie, które mogą przechowywać 256 możliwych wartości lub 100/256, co marnuje około 60% możliwych wartości bajtu.)
Duncan C

16
@Jacksonkr nadal myślisz w bazie 10. Komputery są typu base-2.
Joel Coehoorn

306

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.

(Pierwotnie opublikowane na Quora.)


3
Pamiętaj, że niektóre języki zawierają dokładną matematykę. Jednym z przykładów jest Schemat, na przykład za pośrednictwem GNU Guile. Zobacz draketo.de/english/exact-math-to-the-rescue - te zachowują matematykę jako ułamki i tylko kroją na końcu.
Arne Babenhauserheide

5
@ FloatingRock W rzeczywistości bardzo niewiele głównych języków programowania ma wbudowane liczby wymierne. Arne, podobnie jak ja, jest Schemerem, więc psujemy się.
Chris Jester-Young,

5
@ArneBabenhauserheide Myślę, że warto dodać, że będzie to działać tylko z liczbami wymiernymi. Więc jeśli robisz matematykę z nieracjonalnymi liczbami, takimi jak pi, musisz zapisać ją jako wielokrotność liczby pi. Oczywiście wszelkie obliczenia dotyczące pi nie mogą być reprezentowane jako dokładna liczba dziesiętna.
Aidiakapi,

13
@connexo OK. Jak zaprogramujesz rotator do pizzy, aby uzyskać 36 stopni? Co to jest 36 stopni? (Wskazówka: jeśli potrafisz to dokładnie zdefiniować, masz również plaster do dokładania dziesiątej pizzy). Innymi słowy, tak naprawdę nie możesz mieć 1/360 (stopnia) lub 1 / 10 (36 stopni) z tylko binarną liczbą zmiennoprzecinkową.
Chris Jester-Young,

12
@connexo Ponadto „każdy idiota” nie może obrócić pizzy dokładnie o 36 stopni. Ludzie są zbyt podatni na błędy, aby zrobić coś tak precyzyjnego.
Chris Jester-Young,

212

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.


133
komputery nie potrzebują nieskończonej ilości pamięci, aby uzyskać 0,1 + 0,2 = 0,3 prawo
Pacerier

23
@Pacerier Pewnie, mogliby użyć dwóch liczb całkowitych o nieokreślonej precyzji do reprezentowania ułamka, lub mogliby użyć notacji cytatowej. To specyficzne pojęcie „binarny” lub „dziesiętny” sprawia, że ​​jest to niemożliwe - pomysł, że masz ciąg cyfr binarnych / dziesiętnych i, gdzieś tam, kropkę. Aby uzyskać precyzyjne racjonalne wyniki, potrzebowalibyśmy lepszego formatu.
Devin Jeanpierre

15
@Pacerier: Ani binarne, ani dziesiętne zmiennoprzecinkowe nie mogą dokładnie przechowywać 1/3 ani 1/13. Dziesiętne typy zmiennoprzecinkowe mogą precyzyjnie przedstawiać wartości postaci M / 10 ^ E, ale są mniej precyzyjne niż binarne liczby zmiennoprzecinkowe o podobnych rozmiarach, jeśli chodzi o reprezentowanie większości innych ułamków . W wielu aplikacjach bardziej przydatne jest posiadanie większej precyzji z dowolnymi ułamkami niż doskonała precyzja z kilkoma „specjalnymi”.
supercat

13
@Pacerier Robią , jeśli przechowują liczby jako zmiennoprzecinkowe, co było sednem odpowiedzi.
Mark Amery

3
@chux: Różnica w precyzji między typami binarnymi i dziesiętnymi nie jest ogromna, ale różnica 10: 1 w przypadku najlepszego przypadku w najgorszym przypadku w przypadku typów dziesiętnych jest znacznie większa niż różnica 2: 1 w przypadku typów binarnych. Zastanawiam się, czy ktoś zbudował sprzęt lub oprogramowanie napisane w celu wydajnego działania na jednym z typów dziesiętnych, ponieważ żadne nie wydaje się być podatne na wydajną implementację w sprzęcie lub oprogramowaniu.
supercat

121

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.3powraca falsew 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: 2550centów zamiast 25.50dolarów.


1 Douglas Crockford: JavaScript: Dobre części : Dodatek A - Okropne części (strona 105) .


3
Problem polega na tym, że sama konwersja jest niedokładna. 16,08 * 100 = 1607.9999999999998. Czy musimy uciekać się do dzielenia liczby i przeliczania osobno (jak w 16 * 100 + 08 = 1608)?
Jason

38
Rozwiązaniem jest wykonanie wszystkich obliczeń w liczbach całkowitych, a następnie podzielenie przez proporcje (w tym przypadku 100) i zaokrąglenie tylko podczas prezentacji danych. To zapewni, że twoje obliczenia będą zawsze dokładne.
David Granado,

15
Wystarczy trochę podważyć: arytmetyka liczb całkowitych jest dokładna tylko w liczbach zmiennoprzecinkowych do punktu (zamierzona gra słów). Jeśli liczba jest większa niż 0x1p53 (aby użyć szesnastkowej notacji zmiennoprzecinkowej Java 7, = 9007199254740992), to ulp wynosi 2 w tym punkcie, a więc 0x1p53 + 1 jest zaokrąglany w dół do 0x1p53 (a 0x1p53 + 3 jest zaokrąglany w górę do 0x1p53 + 4, z powodu zaokrągleń do parzystości). :-D Ale na pewno, jeśli twoja liczba jest mniejsza niż 9 biliardów, powinieneś być w porządku. :-P
Chris Jester-Young

2
Jason, powinieneś po prostu zaokrąglić wynik (int) (16.08 * 100 + 0,5)
Michaił Semenow

@CodyBugstein „ Jak uzyskać .1 + .2, aby wyświetlić .3? ” Napisz niestandardową funkcję drukowania, aby wstawić ułamek dziesiętny tam, gdzie chcesz.
RonJohn,

113

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:

  • Pierwszy bit jest bitem znaku : 1jeśli liczba jest ujemna, w 0przeciwnym razie 1 .
  • Następne 11 bitów to wykładnik wykładniczy , który jest przesunięty o 1023. Innymi słowy, po odczytaniu bitów wykładniczych z liczby podwójnej precyzji, 1023 należy odjąć, aby uzyskać potęgę dwóch.
  • Pozostałe 52 bity są znaczeniem (lub mantysą). W mantysie „domniemane” 1.jest zawsze pomijane 2, ponieważ najbardziej znaczącym bitem dowolnej wartości binarnej jest 1.

1 - IEEE 754 pozwala na koncepcję zerowego znaku - +0i -0są 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 1S) 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:...00111...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.2w 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.2by to dało 0.3: kilka ostatnich bitów jest zaokrąglanych.


14
Moja odpowiedź została odrzucona wkrótce po opublikowaniu. Od tego czasu wprowadziłem wiele zmian (w tym wyraźne odnotowanie powtarzających się bitów podczas pisania binarnego 0.1 i 0.2, które pominąłem w oryginale). Jeśli nie ma szans, że głosujący na to zagłosuje, czy mógłbyś przekazać mi swoją opinię, aby poprawić moją odpowiedź? Wydaje mi się, że moja odpowiedź dodaje coś nowego, ponieważ traktowanie sumy w IEEE 754 nie jest ujęte w ten sam sposób w innych odpowiedziach. Chociaż „Co każdy informatyk powinien wiedzieć ...” dotyczy tego samego materiału, moja odpowiedź dotyczy konkretnie przypadku 0,1 + 0,2.
Wai Ha Lee

57

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.1byłby 1 x 10⁻¹, 0.2byłby 2 x 10⁻¹i 0.3byłby 3 x 10⁻¹. Matematyka liczb całkowitych jest łatwa i dokładna, więc dodawanie 0.1 + 0.2oczywiś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.5jest 1 x 2⁻¹i 0.25jest 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.1jest 3602879701896397 x 2⁻⁵⁵, a najbliższą liczbą 0.2jest 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.


2
@ Mark Dziękuję za to jasne wyjaśnienie, ale pojawia się pytanie, dlaczego 0,1 + 0,4 sumuje się dokładnie do 0,5 (przynajmniej w Pythonie 3). Jaki jest najlepszy sposób sprawdzenia równości przy użyciu pływaków w Pythonie 3?
pchegoor

2
@ user2417881 Operacje zmiennoprzecinkowe IEEE mają reguły zaokrąglania dla każdej operacji, a czasami zaokrąglanie może dać dokładną odpowiedź, nawet jeśli dwie liczby są nieco wyłączone. Szczegóły są za długie, by je komentować, i tak nie jestem ekspertem od nich. Jak widać w tej odpowiedzi, 0,5 jest jednym z niewielu miejsc po przecinku, które można przedstawić w postaci binarnej, ale to tylko zbieg okoliczności. Informacje na temat testowania równości znajdują się na stackoverflow.com/questions/5595425/… .
Mark Ransom,

1
@ user2417881 twoje pytanie mnie zaintrygowało, więc przekształciłem je w pełne pytanie i odpowiedź: stackoverflow.com/q/48374522/5987
Mark Ransom

47

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.


33

Moje obejście:

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

precyzja odnosi się do liczby cyfr, które chcesz zachować po przecinku podczas dodawania.


30

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

wprowadź opis zdjęcia tutaj

Nawet jeśli możesz 0.2łatwo pisać , FLT_RADIXa DBL_RADIXjest 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ń.


28

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%).


28

Nie, nie rozbite, ale większość ułamków dziesiętnych musi być przybliżona

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.


Zaokrąglanie do najbliższej liczby całkowitej nie jest bezpiecznym sposobem rozwiązania problemu porównania we wszystkich przypadkach. 0,4999998 i 0,500001 zaokrągla do różnych liczb całkowitych, więc wokół każdego punktu zaokrąglania znajduje się „strefa zagrożenia”. (Wiem, że te ciągi dziesiętne prawdopodobnie nie są dokładnie reprezentowalne, ponieważ binarne zmiennoprzecinkowe IEEE.)
Peter Cordes

1
Ponadto, mimo że zmiennoprzecinkowa jest „starszym” formatem, jest bardzo dobrze zaprojektowana. Nie wiem nic, co ktokolwiek by zmienił, gdyby teraz go przeprojektował. Im więcej się o tym dowiaduję, tym bardziej uważam, że jest naprawdę dobrze zaprojektowany. np. tendencyjny wykładnik oznacza, że ​​kolejne binarne zmiennoprzecinkowe mają kolejne reprezentacje liczb całkowitych, dzięki czemu można zaimplementować 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).
Peter Cordes,

Nie zgadzam się, zmiennoprzecinkowe powinny być przechowywane w postaci dziesiętnej, a nie binarnej, a wszystkie problemy zostały rozwiązane.
Ronen Festinger

Czy „ x / (2 ^ n + 5 ^ n) ” nie powinno być „ x / (2 ^ n * 5 ^ n) ”?
Wai Ha Lee

@RonenFestinger - co z 1/3?
Stephen C

19

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 **


19

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.2i 0.7to 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 / 10to 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ą.


to sprawiło mi prawdziwy ból głowy. Sumuję 12 liczb zmiennoprzecinkowych, a następnie pokazuję sumę i średnią, jeśli te liczby. użycie toFixed () może naprawić sumowanie 2 liczb, ale przy sumowaniu kilku liczb skok jest znaczący.
Nuryagdy Mustapayev

@Nuryagdy Mustapayev Nie dostałem twojej intencji, ponieważ testowałem, zanim możesz zsumować 12 liczb zmiennoprzecinkowych, a następnie użyj funkcji floatify () na wyniku, a następnie zrób co chcesz, nie zauważyłem problemu z jej użyciem.
Mohammad Musavi

Mówię tylko, że w mojej sytuacji, w której mam około 20 parametrów i 20 formuł, w których wynik każdej formuły zależy od innych, to rozwiązanie nie pomogło.
Nuryagdy Mustapayev

16

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.


W ogóle nie rozumiem twojego drugiego akapitu.
Nae

1
@Nae Chciałbym przetłumaczyć drugi akapit jako: „Większość ułamków nie może być reprezentowana dokładnie w postaci dziesiętnej ani binarnej. Więc większość wyników zostanie zaokrąglona - chociaż nadal będą one precyzyjne względem liczby bitów / cyfr nieodłącznie związanych z reprezentacją używane ”.
Steve Summit

15

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 floatlub doubledo BigDecimalw 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.


2
Do osoby, której edycję właśnie wycofałem: uważam, że cytowanie kodu jest odpowiednie do cytowania kodu. Ta odpowiedź, bez względu na język, nie zawiera żadnego cytowanego kodu. Liczb można używać w zdaniach angielskich, co nie zamienia ich w kod.
Patricia Shanahan

To jest prawdopodobnie dlaczego ktoś sformatowane numery jako kod - nie do formatowania, ale dla czytelności.
Wai Ha Lee

... Również runda nawet odnosi się do binarnej reprezentacji, nie dziesiętnej reprezentacji. Zobacz to lub na przykład to .
Wai Ha Lee

@WaiHaLee Nie zastosowałem testu nieparzystego / parzystego do liczb dziesiętnych, tylko w systemie szesnastkowym. Cyfra szesnastkowa jest nawet wtedy i tylko wtedy, gdy najmniej znaczącym bitem jej binarnej ekspansji jest zero.
Patricia Shanahan,

14

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:

  • decimalModuł Pythona i BigDecimalklasa 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
    

    decimalModuł Pythona oparty jest na standardzie IEEE 854-1987 .

  • fractionsModuł Pythona i BigFractionklasa 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ą.


14

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


Ponieważ ludzie używają liczb dziesiętnych, nie widzę żadnego dobrego powodu, dla którego zmiennoprzecinkowe nie są domyślnie przedstawiane jako dziesiętne, więc mamy dokładne wyniki.
Ronen Festinger

Ludzie używają wielu baz innych niż baza 10 (dziesiętne), przy czym binarnie jest tym, którego najczęściej używamy do obliczeń. „Dobrym powodem” jest to, że po prostu nie możesz reprezentować każdej frakcji w każdej bazie ..

Binarną arytmetykę @RonenFestinger można łatwo zaimplementować na komputerach, ponieważ wymaga ona tylko ośmiu podstawowych operacji na cyfrach: powiedz $ a $, $ b $ w 0,1 $ $ wszystko, co musisz wiedzieć, to $ \ nazwa operatora {xor} (a, b) $ i $ \ nazwa operatora {cb} (a, b) $, gdzie xor jest wyłączny lub a cb jest „bitem przenoszenia”, który wynosi 0 $ we wszystkich przypadkach, z wyjątkiem gdy $ a = 1 = b $, w którym to przypadku mamy jeden (w rzeczywistości przemienność wszystkich operacji pozwala zaoszczędzić 2 $ skrzynek i wszystko czego potrzebujesz to 6 $ reguł). Rozszerzenie dziesiętne wymaga przechowywania 10 $ \ razy 11 $ (w zapisie dziesiętnym) skrzynek i 10 $ różnych stanów dla każdego bitu i marnuje miejsce na przenoszenie.
Oskar Limka

@RonenFestinger - Dziesiętny nie jest dokładniejszy. Tak mówi ta odpowiedź. Dla każdej wybranej przez ciebie bazy będą liczby wymierne (ułamki), które dają nieskończenie powtarzające się sekwencje cyfr. Dla przypomnienia, jedne z pierwszych komputerów zrobił pomocą bazy 10 reprezentacje liczb, ale pionierskie projektanci sprzętu komputerowego wkrótce stwierdził, że podstawa 2 było o wiele łatwiejsze i bardziej efektywne wdrożenie.
Stephen C

9

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.


9

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ę, xxtak 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ś bcdrukowałem sumę terminów wyprowadzanych przez program główny. Tę sumę można wstawić do pytona repllub 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ć scalewspółczynnika bc.

Wyświetlana suma jest zawarta w sprzęcie. Wartość uzyskana przez obliczenie zależy od ustawionej skali. Ustawiłem scalewspółczynnik na 15. Matematycznie, z nieskończoną precyzją, wydaje się, że jest to 1 000 000 000.


5

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.


4

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.00000000dokł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.0100110i 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.0001100moż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.


4

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

3

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


3

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,

Od https://0.30000000000000004.com/


3

Liczby dziesiętne, takie jak 0.1, 0.2i 0.3nie są reprezentowane dokładnie w typach zmiennoprzecinkowych kodowanych binarnie. Suma aproksymacji 0.1i 0.2różni się od aproksymacji zastosowanej dla 0.3, stąd kłamstwo, 0.1 + 0.2 == 0.3co 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, _Decimal64i _Decimal128typy 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 ).


1

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


1
Nie jestem jednak pewien, czy odpowiedziałeś na pytanie „ Dlaczego zdarzają się te nieścisłości? ”.
Wai Ha Lee,

w pewnym sensie masz rację, ale przyszedłem tutaj z javascript dziwnego zachowania w tej kwestii ... chcę tylko podzielić się rodzajem rozwiązania
bortunac 10.10.19

0

Właśnie zobaczyłem ten interesujący problem wokół zmiennoprzecinkowych:

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

Wpisz opis zdjęcia tutaj

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)

Wpisz opis zdjęcia tutaj

Rzeczywista wartość przyjęta przez dany 64-bitowy układ podwójnej precyzji z danym tendencyjnym wykładnikiem i ułamkiem 52-bitowym wynosi

Wpisz opis zdjęcia tutaj

lub

Wpisz opis zdjęcia tutaj

Dzięki @a_guest za wskazanie mi tego.


-1

Inne pytanie zostało nazwane jako duplikat tego:

W C ++ dlaczego wynik cout << xróżni się od wartości, dla której pokazuje debugger x?

xW pytaniu jest floatzmienna.

Przykładem może być

float x = 9.9F;

Debuger pokazuje 9.89999962, że wynikiem coutdziałania jest 9.9.

Okazuje się, że coutdomyślna precyzja floatwynosi 6, więc zaokrągla do 6 cyfr dziesiętnych.

Zobacz tutaj w celach informacyjnych


1
IMO - opublikowanie tego tutaj było złym podejściem. Wiem, że jest to frustrujące, ale ludzie, którzy potrzebują odpowiedzi na pierwotne pytanie (najwyraźniej teraz usunięte!) Nie znajdą go tutaj. Jeśli naprawdę czujesz, że twoja praca zasługuje na uratowanie, sugerowałbym: 1) poszukiwanie innego pytania, na które tak naprawdę odpowiada, 2) stworzenie pytania z odpowiedzią.
Stephen C
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.