Arytmetyka zmiennoprzecinkowa: nie taki diabeł straszny jak go malują

https://xpil.eu/dy9

Programiści (albo bardziej ogólnie: fachowcy od komputerowego przetwarzania danych) dzielą się na dwa obozy: jedni twierdzą, że używanie danych zmiennoprzecinkowych jest fajne, wygodne i bardzo, bardzo precyzyjne - a drudzy, że ze względu na zaokrąglenia, jakie występują na ostatniej (najmniej znaczącej) pozycji operacjom zmiennoprzecinkowym ufać nie można i należy ich unikać jak diabeł święconej wody.

Zanim pokażę o ssso chozzzzi, najpierw pokażę o ssso chozzzzzi.

Znaczy, tego. Zanim pokażę o co chodzi z tym całym konfliktem, najpierw króciutkie wprowadzenie w temat:

Wyobraźmy sobie, że mamy przemnożyć przez siebie liczby 1.2 oraz 1.3. Z tym tylko, że mamy do dyspozycji tylko jedno miejsce po przecinku. Wszędzie, w całym procesie mnożenia, możemy operować wyłącznie na liczbach z jednym miejscem po przecinku.

Co więc robimy?

Bez wnikania w szczegółową implementację operacji mnożenia, napiszę tylko tyle: zamiast spodziewanego wyniku 1.56 dostaniemy wynik 1.6.

Dlaczego tak?

Bo mamy do dyspozycji tylko jedno miejsce dziesiętne po przecinku i lepiej się nie da.

Czy taki wynik jest poprawny?

Raczej nie za bardzo. Walnęliśmy się o całe cztery setne. Sporo.

Ale biorąc pod uwagę ograniczenia, jakim podlegaliśmy, to najlepszy wynik, jaki możemy uzyskać. Koniec, kropka.

Jeżeli teraz odwrócimy operację i spróbujemy wynik mnożenia (1.2 x 1.3) podzielić przez 1.2, to na wyjściu dostaniemy... 1.3. Dlaczego? Dlatego, że 1.6/1.2=1.3333... Czyli po zaokrągleniu do jednego miejsca: 1.3.

Na razie wszystko ładnie. O co więc bić pianę?

Okazuje się, że nie zawsze jest tak różowo jak w przykładzie powyżej. Czym bardziej mnożone liczby będą się różnić, tym większa szansa, że po przemnożeniu i podzieleniu wynik wyjdzie odrobinę przesunięty.

Przykład:

0.2 x 1.7 = 0.34, ale ponieważ mamy tylko jedno miejsce po przecinku, robi się z tego 0.3. Teraz "odwracamy" działanie, czyli dzielimy wynik ostatniego mnożenia przez 0.2:

0.3 / 0.2 = 1.5

Zamiast 1.7 dostaliśmy 1.5, czyli aż o dwie dziesiąte mniej niż poprawne 1.7.

Ktoś powie: zaraz, zaraz, ale dlaczego ograniczamy się tylko do jednego miejsca dziesiętnego? Przecież współczesne komputery mają o wiele większą dokładność!

Owszem. Mają. Ale zawsze skończoną. I każdy ułamek dziesiętny gdzieś tam będzie miał ostatnią cyfrę, która po przemnożeniu i podzieleniu zamieni się na coś innego.

No to teraz wracamy do clue, czyli odwiecznego sporu: fajne są te zmiennoprzecinkowe, czy niefajne?

Odpowiedź - jak to często bywa - leży gdzieś po środku i rechocze. Brzmi ona: to zależy.

Jeżeli osobnik używający zmiennych typu float będzie tępo wierzył w doskonałą precyzję, to prędzej czy później natrafi na problem trudny do zdiagnozowania. Przykład:

a := 1.0 / 3.0; // wynik: zmienna a równa jest 0.333333333333333333333333333

b := a + a + a; // wynik: zmienna b równa jest 0.999999999999999999999999999

if(b==1.0) {

// ten blok kodu nigdy się nie wykona, bo 1 != 0.999999999999999999999999999

}

A więc przykazanie #1: nigdy, przenigdy nie stawiać zmiennych (ani stałych!) typu float po obu stronach operatora porównania.

A co, jeżeli musimy, bezwzględnie musimy stwierdzić, czy te dwie zmienne typu float są równe czy nie?

Wówczas zamiast je porównywać, należy je odjąć, a następnie sprawdzić, czy moduł różnicy jest dostatecznie blisko zera.

Jeżeli pracujemy w środowisku bankowym, wówczas - na ogół - wystarczy nam dokładność do czterech miejsc po przecinku przy operacjach pojedynczych oraz sześciu lub ośmiu przy operacjach "zbiorczych" . I nie mówię tu o prostych saldach, tylko o wyliczaniu różnych wskaźników, do których klient na ogół nie ma za bardzo dostępu, typu symulacje "co by było gdyby" i tak dalej. I wtedy jeżeli chcemy porównać ze sobą dwie zmienne typu float, robimy:

if(abs(a-b) < 0.000000001)

{

// ten blok kodu ma szansę się wykonać, jeżeli a równa się b z dokładnością do dziewięciu miejsc po przecinku

}

Najważniejsze więc to używać narzędzi z głową. Rozumieć jak są skonstruowane i jak za ich pomocą osiągnąć oczekiwane wyniki.

Ot i cała tajemnica...

 

https://xpil.eu/dy9

2 komentarze

Leave a Comment

Komentarze mile widziane.

Jeżeli chcesz do komentarza wstawić kod, użyj składni:
[code]
tutaj wstaw swój kod
[/code]

Jeżeli zrobisz literówkę lub zmienisz zdanie, możesz edytować komentarz po jego zatwierdzeniu.