Dlaczego wielokrotne dodawanie 0,1 pozostaje bezstratne?

152

Wiem, że 0.1liczby dziesiętnej nie można dokładnie przedstawić za pomocą skończonej liczby binarnej ( wyjaśnienie ), więc double n = 0.1straci pewną precyzję i nie będzie dokładnie 0.1. Z drugiej strony 0.5może być reprezentowany właśnie dlatego, że jest 0.5 = 1/2 = 0.1b.

Powiedziawszy, że jest zrozumiałe, 0.1 trzykrotne dodanie nie da dokładnie, 0.3więc wypisuje następujący kod false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Ale jak to się dzieje, że dodanie 0.1 pięciu razy da dokładnie 0.5? Następujący kod drukuje true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Jeśli 0.1nie można go dokładnie przedstawić, jak to się dzieje, że dodanie go 5 razy daje dokładnie to, 0.5co można dokładnie przedstawić?

icza
źródło
7
Jeśli naprawdę to zbadasz, jestem pewien, że możesz to rozgryźć, ale zmiennoprzecinkowe są pełne „niespodzianek” i czasami lepiej jest po prostu spojrzeć na to ze zdumieniem.
Hot Licks
3
Myślisz o tym w sposób matematyczny. Arytmetyka zmiennoprzecinkowa w żaden sposób nie jest matematyką.
Jakob
13
@HotLicks że jest bardzo dużo zła postawa mieć.
hobbs
2
@RussellBorogove, nawet jeśli został zoptymalizowany, byłaby poprawną optymalizacją tylko wtedy, gdyby summiała taką samą wartość końcową, jak gdyby pętla została naprawdę wykonana. W standardzie C ++ nazywa się to „regułą as-if” lub „tym samym obserwowalnym zachowaniem”.
Hobbs
7
@Jakob w ogóle nie jest prawdziwy. Arytmetyka zmiennoprzecinkowa jest rygorystycznie zdefiniowana, z dobrym matematycznym traktowaniem granic błędów i tym podobnych. Po prostu wielu programistów albo nie chce kontynuować analizy, albo błędnie wierzy, że „zmiennoprzecinkowe jest niedokładne” jest wszystkim, co trzeba wiedzieć, a analiza nie jest warta zawracania sobie głowy.
Hobbs

Odpowiedzi:

155

Błąd zaokrąglania nie jest przypadkowy i sposób jego implementacji stara się zminimalizować błąd. Oznacza to, że czasami błąd nie jest widoczny lub nie ma błędu.

Na przykład 0.1nie jest dokładnie 0.1tj., new BigDecimal("0.1") < new BigDecimal(0.1)Ale 0.5jest dokładnie1.0/2

Ten program pokazuje prawdziwe wartości.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

wydruki

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Uwaga: to 0.3jest nieco wyłączone, ale kiedy dojdziesz do 0.4bitów, musisz przesunąć o jeden w dół, aby zmieścić się w 53-bitowym limicie, a błąd jest odrzucany. Ponownie, skrada się do tyłu o błędach 0.6i 0.7lecz 0.8się 1.0błąd zostanie odrzucony.

Dodanie go 5 razy powinno skumulować błąd, a nie anulować.

Przyczyną wystąpienia błędu jest ograniczona precyzja. tj. 53-bity. Oznacza to, że ponieważ liczba zużywa więcej bitów, gdy staje się większa, bity muszą być odrzucane z końca. Powoduje to zaokrąglanie, które w tym przypadku jest na Twoją korzyść.
Możesz uzyskać odwrotny efekt, gdy uzyskasz mniejszą liczbę, np. 0.1-0.0999=> 1.0000000000000286E-4 I zobaczysz więcej błędów niż wcześniej.

Przykładem tego jest dlaczego w Javie 6 Dlaczego Math.round (0.49999999999999994) zwraca 1 W tym przypadku utrata bitu w obliczeniach skutkuje dużą różnicą w odpowiedzi.

Peter Lawrey
źródło
1
Gdzie to jest realizowane?
EpicPandaForce
16
@Zhuinden Procesor jest zgodny ze standardem IEEE-754. Java zapewnia dostęp do podstawowych instrukcji procesora i nie angażuje się. en.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey
10
@PeterLawrey: Niekoniecznie procesor. Na komputerze bez zmiennoprzecinkowego procesora (i bez używanego oddzielnego FPU) arytmetyka IEEE będzie wykonywana przez oprogramowanie. A jeśli procesor hosta ma zmiennoprzecinkowe, ale nie spełnia wymagań IEEE, myślę, że implementacja Java dla tego procesora byłaby zobowiązana również do użycia miękkiego float ...
R .. GitHub STOP HELPING ICE
1
@R .. w takim przypadku nie wiem, co by się stało, gdybyś użył strictfp Time do rozważenia liczb całkowitych o stałym punkcie. (lub BigDecimal)
Peter Lawrey
2
@eugene kluczowym problemem są ograniczone wartości, które mogą przedstawiać zmiennoprzecinkowe. To ograniczenie może spowodować utratę informacji, a wraz ze wzrostem liczby utratę błędu. Używa zaokrąglania, ale w tym przypadku zaokrągla w dół, więc liczba, która byłaby nieco za duża, ponieważ 0,1 jest nieco za duża, zamienia się w poprawną wartość. Dokładnie 0,5
Peter Lawrey
47

Zakaz przepełnienia, w zmiennoprzecinkowym, x + x + xjest dokładnie zaokrągloną (tj. Najbliższą) liczbą zmiennoprzecinkową do rzeczywistej 3 * x, x + x + x + xwynosi dokładnie 4 * xi x + x + x + x + xponownie jest poprawnie zaokrąglonym przybliżeniem zmiennoprzecinkowym dla 5 * x.

Pierwszy wynik x + x + xwynika z faktu, że x + xjest dokładny. x + x + xjest zatem wynikiem tylko jednego zaokrąglenia.

Drugi wynik jest trudniejszy, jeden z jego wykazów omówiono tutaj (a Stephen Canon nawiązuje do innego dowodu poprzez analizę przypadku na ostatnich 3 cyfrach x). Podsumowując, albo 3 * xjest w tej samej binadzie co 2 * xalbo jest w tej samej binadzie co 4 * x, iw każdym przypadku można wywnioskować, że błąd trzeciego dodawania anuluje błąd drugiego dodawania ( pierwszy dodatek jest dokładny, jak już powiedzieliśmy).

Trzeci wynik, „ x + x + x + x + xjest prawidłowo zaokrąglony”, pochodzi z drugiego w taki sam sposób, w jaki pierwszy wynika z dokładności x + x.


Drugi wynik wyjaśnia, dlaczego 0.1 + 0.1 + 0.1 + 0.1jest to liczba zmiennoprzecinkowa: liczby 0.4wymierne 1/10 i 4/10 są aproksymowane w ten sam sposób, z tym samym błędem względnym, po konwersji na zmiennoprzecinkowe. Te liczby zmiennoprzecinkowe mają stosunek dokładnie 4 między nimi. Pierwszy i trzeci wynik pokazują, że 0.1 + 0.1 + 0.1i 0.1 + 0.1 + 0.1 + 0.1 + 0.1można oczekiwać, że będą miały mniej błędów, niż można by wywnioskować na podstawie naiwnej analizy błędów, ale same w sobie odnoszą się one tylko do wyników odpowiednio 3 * 0.1i 5 * 0.1, co do których można oczekiwać, że będą bliskie, ale niekoniecznie identyczne z 0.3i 0.5.

Jeśli będziesz kontynuować dodawanie 0.1po czwartym dodaniu, w końcu zauważysz błędy zaokrągleń, które powodują, że „ 0.1dodane do siebie n razy” odbiegają n * 0.1od n / 10, a jeszcze bardziej odbiegają od n / 10. Gdybyś miał wykreślić wartości „0,1 dodane do siebie n razy” jako funkcję n, zobaczyłbyś linie o stałym nachyleniu w binadach (gdy tylko wynik n-tego dodania ma wpaść do określonej binady, można oczekiwać, że właściwości dodatku będą podobne do poprzednich dodatków, które dały wynik w tej samej binadzie). W tej samej binadzie błąd będzie się zwiększał lub zmniejszał. Gdybyś spojrzał na sekwencję zboczy od binade do binade, rozpoznałbyś powtarzające się cyfry0.1binarnie przez chwilę. Po tym nastąpiłaby absorpcja i krzywa byłaby płaska.

Pascal Cuoq
źródło
1
W pierwszym wierszu mówisz, że x + x + x jest dokładnie poprawne, ale z przykładu w pytaniu tak nie jest.
Alboz
2
@Alboz Mówię, że x + x + xjest to dokładnie poprawnie zaokrąglona liczba zmiennoprzecinkowa do rzeczywistej 3 * x. „Prawidłowo zaokrąglony” oznacza „najbliższy” w tym kontekście.
Pascal Cuoq,
4
+1 To powinna być zaakceptowana odpowiedź. W rzeczywistości oferuje wyjaśnienia / dowody tego, co się dzieje, a nie tylko niejasne ogólniki.
R .. GitHub STOP HELPING ICE
1
@Alboz (wszystko to jest przewidziane w pytaniu). Ale ta odpowiedź wyjaśnia, w jaki sposób błędy przypadkowo kasują się, a nie sumują w najgorszy możliwy sposób.
Hobbs
1
@chebus 0.1 to 0x1.999999999999999999999… p-4 w systemie szesnastkowym (nieskończona sekwencja cyfr). Jest przybliżany z podwójną precyzją jako 0x1.99999ap-4. 0,2 to 0x1.999999999999999999999… p-3 w systemie szesnastkowym. Z tego samego powodu, dla którego 0,1 jest aproksymowane jako 0x1,99999ap-4, 0,2 jest aproksymowane jako 0x1,99999ap-3. Tymczasem 0x1.99999ap-3 to również dokładnie 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq
-1

Systemy zmiennoprzecinkowe wykonują różne magie, w tym mają kilka dodatkowych bitów precyzji do zaokrąglania. Tak więc bardzo mały błąd spowodowany niedokładną reprezentacją 0,1 kończy się zaokrągleniem do 0,5.

Pomyśl o zmiennoprzecinkowych jako świetnym, ale NIESKŁADNYM sposobie przedstawiania liczb. Nie wszystkie możliwe liczby można łatwo przedstawić na komputerze. Liczby nieracjonalne, takie jak PI. Lub jak SQRT (2). (Symboliczne systemy matematyczne mogą je reprezentować, ale powiedziałem „łatwo”).

Wartość zmiennoprzecinkowa może być bardzo bliska, ale nie dokładna. Może być tak blisko, że możesz nawigować do Plutona i oddalać się o kilka milimetrów. Ale nadal nie jest dokładne w sensie matematycznym.

Nie używaj zmiennoprzecinkowych, gdy chcesz być dokładny, a nie przybliżony. Na przykład aplikacje księgowe chcą dokładnie śledzić określoną liczbę groszy na koncie. Liczby całkowite są do tego dobre, ponieważ są dokładne. Podstawowym problemem, na który należy zwrócić uwagę w przypadku liczb całkowitych, jest przepełnienie.

Używanie BigDecimal dla waluty działa dobrze, ponieważ podstawową reprezentacją jest liczba całkowita, choć duża.

Uznając, że liczby zmiennoprzecinkowe są niedokładne, nadal mają wiele zastosowań. Systemy współrzędnych do nawigacji lub współrzędne w systemach graficznych. Wartości astronomiczne. Wartości naukowe. (Prawdopodobnie i tak nie możesz znać dokładnej masy piłki baseballowej do masy elektronu, więc niedokładność nie ma znaczenia).

W przypadku aplikacji liczących (w tym księgowych) użyj liczb całkowitych. Aby policzyć liczbę osób przechodzących przez bramę, użyj wartości int lub long.

DannyB
źródło
2
Pytanie jest oznaczone tagiem [java]. Definicja języka Java nie przewiduje „kilku dodatkowych bitów precyzji”, a jedynie kilka dodatkowych bitów wykładnika (i to tylko wtedy, gdy nie używasz strictfp). Tylko dlatego, że wyrzekłeś się zrozumienia czegoś, nie oznacza, że ​​jest to niezgłębione, ani że inni powinni wyrzec się zrozumienia tego. Zobacz stackoverflow.com/questions/18496560 jako przykład długości implementacji języka Java w celu zaimplementowania definicji języka (która nie zawiera żadnych przepisów dotyczących bitów o dodatkowej precyzji, ani strictfpżadnych dodatkowych bitów exp)
Pascal Cuoq