2013-02-13 15 views
6

piszę pętlę sumujący z pływakiem, ale nie natknąłem się na arytmetyki zmiennoprzecinkowej problemu przedstawionego w poniższym przykładzie:C++ Jak uniknąć zmiennoprzecinkową błąd arytmetyczny

for(float value = -2.0; value <= 2.0; value += 0.2) 
    std::cout << value << std::endl; 

Oto wyjście:

-2 
-1.8 
-1.6 
-1.4 
-1.2 
-1 
-0.8 
-0.6 
-0.4 
-0.2 
1.46031e-07 
0.2 
0.4 
0.6 
0.8 
1 
1.2 
1.4 
1.6 
1.8 

Dlaczego właśnie ja się 1.46031e-07 zamiast 0? Wiem, że ma to coś wspólnego z błędami zmiennoprzecinkowymi, ale nie mogę zrozumieć, dlaczego tak się dzieje i co powinienem zrobić, aby temu zapobiec (jeśli jest jakiś sposób). Czy ktoś może wyjaśnić (lub wskazać mi link), który pomoże mi zrozumieć? Dowolne wejście jest doceniane. Dzięki!

+4

"Jak uniknąć arytmetyki zmiennoprzecinkowej błąd" - nie można, przepraszam. –

+0

to pytanie zostało zadane i wiele razy odpowiedziano: – bernie

+3

'0.2' nie może być dokładnie reprezentowane przez' float' (przy założeniu arytmetyki zmiennoprzecinkowej IEEE754). Możesz to zobaczyć, jeśli zwiększysz precyzję wyniku: [przykład] (http://liveworkspace.org/code/3ZXIxx$0). – Mankarse

Odpowiedz

7

Dzieje się tak dlatego, że numery zmiennoprzecinkowe mają tylko pewną dyskretną dokładność.

0.2 nie jest tak naprawdę 0,2, ale jest wewnętrznie reprezentowana jako nieznacznie inna liczba.

Dlatego właśnie widzisz różnicę.

Jest to typowe we wszystkich obliczeniach zmiennoprzecinkowych i naprawdę nie można tego uniknąć.

+1

Należy zauważyć, że podczas gdy 0.2 nie może być dokładnie reprezentowane jako zmiennoprzecinkowe, -2,0 i 2.0 może. Zwracam na to uwagę, aby uniknąć wrażenia, że ​​matematyka zmiennoprzecinkowa jest arbitralna i kapryśna. Wszystko, co się dzieje, to pływająca i podwójnie używana podstawa 2, a 0.2 jest równoważne 1/5, której nie można przedstawić jako skończonej liczby 2 podstawy. -2, 2,0, 0,5, 0,25, -375 i 178432 mogą być dokładnie reprezentowane. –

1

Dowiedz się więcej o reprezentacji zmiennoprzecinkowej z niektórymi książkami Algorytmy lub korzystasz z Internetu. Istnieje wiele zasobów.

Na razie wydaje się, że w jakiś sposób można uzyskać zero, gdy jego wartość jest bardzo bliska zeru. i wszyscy wiemy, że nazywamy ten proces "zaokrąglaniem". :) więc dlaczego nie użyjesz go podczas drukowania tych liczb. Funkcja printf zapewnia dobrą siłę formatowania dla tego typu rzeczy. sprawdź tabele w poniższym linku, jeśli nie wiesz, jak formatować za pomocą printf. (Można użyć formatowania do zaokrąglania i wyświetlania prawidłowo numery) printf Ref: http://www.cplusplus.com/reference/cstdio/printf/?kw=printf

- edycja -

może ktoś z was wie, że zgodnie z matematyki 1.99999999 .... jest taka sama jako 2,0. Jedyną różnicą jest reprezentacja. Ale liczba jest taka sama.

Twój problem z liczbą zmiennoprzecinkową to trochę podobny do tego podobnego do. (Jest to po prostu tylko dla swojej wyjaśnienia problem nie jest taki sam jak na 1,9999 .... rzeczą.).

5

użyć liczb całkowitych i podzielić dół:

for(int value = -20; value <= 20; value += 2) 
    std::cout << (value/10.0) << std::endl; 
+0

+1, ale ... przez podzielenie "wartość" przez "10.0", sugerujesz kompilatorowi, że powinien on obliczać z podwójną precyzją, a następnie konwertować na pojedynczą precyzję (program OP, który próbujesz emulować, ma zmienną o pojedynczej precyzji). Tak się składa, że ​​daje to taki sam wynik, jak prosty podział na jedną precyzję. Ale ponieważ powód, dla którego daje identyczne wyniki, jest nietrywialny, kompilator prawie na pewno wygeneruje kod dla podwójnej precyzji dzielenia, a następnie konwersji z podwójnej na pojedynczą precyzję. Z tego powodu wartość "10.0f" byłaby nieznacznie lepsza. –

+0

Właśnie sprawdziłem i GCC generuje podział z jedną precyzją dla 'float r = f/10.0;'. Jestem pod wrażeniem (a mój poprzedni komentarz traci wiele ze swojej wartości). –

23

Jak wszyscy inni mówili, to jest do tego, że liczby rzeczywiste są zbiorem nieskończonym i niepoliczalnym, podczas gdy reprezentacje zmiennoprzecinkowe używają skończonej liczby bitów. Liczby zmiennoprzecinkowe mogą jedynie przybliżać liczby rzeczywiste, a nawet w wielu prostych przypadkach nie są precyzyjne ze względu na ich definicję. Jak widzisz, 0.2 nie jest w rzeczywistości numerem 0.2, ale jest liczbą bardzo zbliżoną do niego. Gdy dodasz je do value, zbierasz błąd w każdym kroku.

Jako alternatywę, spróbuj użyć int s dla iteracji i dzieląc wynik, aby go z powrotem w domenie wymagać:

for (int value = -20; value <= 20; value += 2) { 
    std::cout << (value/10.f) << std::endl; 
} 

Dla mnie to daje:

-2 
-1.8 
-1.6 
-1.4 
-1.2 
-1 
-0.8 
-0.6 
-0.4 
-0.2 
0 
0.2 
0.4 
0.6 
0.8 
1 
1.2 
1.4 
1.6 
1.8 
2 
+2

Nie mogę uwierzyć, że to nie jest akceptowana odpowiedź, ponieważ ta i 1 inna odpowiedź są jedynymi, które dają rozwiązanie. – Celeritas

+0

To przypomina mi słynne problemy związane z [ULPs] (http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition) –

5

Zróbmy twoja pętla, ale ze zwiększoną precyzją wyjściową. Kod

:

for(float value = -2.0; value <= 2.0; value += 0.2) 
    std::cout << std::setprecision(100) << value << std::endl; 

wyjściowa:

-2 
-1.7999999523162841796875 
-1.599999904632568359375 
-1.3999998569488525390625 
-1.19999980926513671875 
-0.999999821186065673828125 
-0.79999983310699462890625 
-0.599999845027923583984375 
-0.3999998569488525390625 
-0.19999985396862030029296875 
1.460313825418779742904007434844970703125e-07 
0.20000015199184417724609375 
0.400000154972076416015625 
0.6000001430511474609375 
0.800000131130218505859375 
1.00000011920928955078125 
1.20000016689300537109375 
1.40000021457672119140625 
1.60000026226043701171875 
1.80000030994415283203125 
Powiązane problemy