Zmienne cz. 3 - liczby zmiennoprzecinkowe - Programowanie jest łatwe

Zmienna zmiennoprzecinkowa - po prostu wartości ułamkowe. Niektórych na matematyce ułamki przyprawiały o ból głowy. Informatyków także. Poznaj zasady działania ułamków w cyfrowym świecie.

Za co programista może pójść do więzienia

W poprzednich ćwiczeniach dokonałem pobieżnej charakterystyki liczb całkowitych o różnych zakresach (int, long int, short int). Do radzenia sobie z wartościami zmiennoprzecinkowymi służy typ float i tak samo, jak w przypadku int, ma swój odpowiednik bez znaku unsigned float. Jednak liczby zmiennoprzecinkowe mają w sobie pewien mroczny zakamarek, przez który odradzam korzystania z nich, chyba, że doskonale wiemy co robimy.

Matematyczne zakamarki

Liczby całkowite są stosunkowo łatwe do umieszczenia w pamięci komputera. Albo jakiś bit jest ustawiony, bądź nie. Pomiędzy 1 a 2 jest dokładnie 1 wartość. Rzecz ma się nieco inaczej, jeżeli chodzi o przechowywanie liczb ułamkowych, za które w matematyce odpowiadają liczby rzeczywiste. Wartości zmiennoprzecinkowych pomiędzy 1 a 2 jest nieskończenie wiele, np pomiędzy następującymi liczbami 1.1, 100000001, 1000000000000000001 jest nieskończenie wiele innych liczb. Klasyczne podejście do zmiennych nie umożliwia zapisania takich wartości. Jednym z rozwiązań tego problemu jest podzielenie wartości rejestru na dwa elementy: wartość całkowitą i wartość zmiennoprzecinkową. To jednak bardzo ogranicza ich zakres wartości. Rozwiązaniem tego problemu (który studenci informatyki zgłębiają na zajęciach z metod numerycznych) jest rozdzielenie wartości zmiennej na dwa elementy: mantysę i eksponentę, a wartość końcowa to wynik mnożenia jednej przez drugą. Jeżeli jesteś na tyle gorliwy, aby badać ten temat, możesz znaleźć więcej informacji np. tutaj. Dla naszych potrzeb musimy wiedzieć tylko i aż tyle: w komputerze nie ma miejsca na każdą wartość zmiennoprzecinkową. Pomiędzy poszczególnymi wartościami są "dziury" a sama dokładność wartości jest obarczona pewnym błędem. Im mniejsza wielkość procesora tym mniejsza precyzja przetwarzania (większe odstępy pomiędzy poszczególnymi wartościami). Konkluzja jest taka, że wpisując do komputera wartość np. 0.1 nie mamy 100% gwarancji, że komputer operuje dokładnie na takiej wartości. Procesor przybliża wynik najlepiej jak tylko może, ale uruchamiając algorytm na takich wartościach możemy się mocno rozczarować, otrzymując ostateczny wynik.

Eksperyment

Poniżej zademonstruję klasyczny, akademicki przykład, który ujawnia niedokładność liczb zmiennoprzecinkowych w komputerach. Za napisanie takiego kodu można pójść do więzienia (serio!), jednak czego nie robi się dla nauki. Rozważmy następujący przykład- napiszmy pętlę while, która ma się wykonać 10 razy. Jako licznik pętli użyjemy zmiennej typu float, którą nazwiemy licznik. Będziemy liczyć od wartości 0. Pętla ma działać tak długo, dopóki licznik nie będzie wynosić 1.

float licznik = 0;
printf ("poczatek petli\n");
while (licznik == 1) {
  licznik += 0.1;
  printf("kolejny obieg petli, wartosc licznika: %f\n", licznik);
}
printf("Koniec petli, wartosc licznika: %f\n", licznik);

Powyższy kod możesz uruchomić np. Tutaj: https://www.onlinegdb.com/online_c_compiler (uważaj, ten kod zawiesi przeglądarkę!).

To, co się stanie, to nieskończona pętla, ponieważ wartość 0.1 w komputerze nie istnieje. Układ bitów w rejestrach przetwarzanych przez procesor da wyniki np. 0.099999999995. Chodzi po prostu o to, że wartość 0.1 nie ma swojego odwzorowania w informatyce. Wartości ułamkowe są obarczone błędem! Dlatego nigdy nie porównuj wartości ułamkowych, aby sprawdzić czy są równe. Zawsze porównuj czy są większe bądź mniejsze. Przykład programu powyżej działałby dobrze, gdyby zamiast znaku == użyto <. Sprawdź.

Za co można pójść do więzienia?

Jak już wcześniej pokazałem, wartości zmiennoprzecinkowe w komputerach nie są dokładne i zawieszenie się programu jest chyba najmniejszym problemem. Są przykłady, gdy błędy (spowodowane przez programistę) były fatalne w skutkach. Najbardziej znanym przykładem był błędny kod w radarach systemu rakiet Patriot. Programista użył wartości zmiennoprzecinkowej dla zmiennej odpowiadającej za licznik czasowy i zwiększał go o.... 0.1 co każdą sekundę. Po około 4 dniach działania systemu zmienna była obarczona błędem około 0.3 sekundy. Rakieta Patriot przemieszcza się z prędkością ok 1500m/s- czyli w ciągu 0.3 sekundy potrafi przelecieć ponad pół kilometra. Niefortunna rakieta trafiła w barak i zabiła 28 żołnierzy. Tymczasowym rozwiązaniem, zanim wprowadzono zaktualizowany kod do systemu radaru, było resetowanie radaru co 24 godziny...

Uncle "Bob" Martin na jednym z wykładów powiedział: "Cannot compare real numbers equal, people go to jail for doing that".

Zwiększona precyzja

Czasami będziemy potrzebować zwiększonej dokładności wartości zmiennoprzecinkowych. To nie uchroni nas przed błędem liczb rzeczywistych, ale czasami może poprawić dokładność wyników. W tym celu mamy dostęp do zmiennej typu double. Różnica w double a float polega na tym, że double przeznacza więcej bitów na mantysę (część ułamkową).

Ćwiczenia

  1. Co się stanie, gdy zapiszesz do zmiennej typu float wartość 1?
  2. Co się stanie, gdy porównasz wartość zmiennej z ćwiczenia 1 ze zmienną typu int o wartości 1?
  3. Jak rozwiążesz problem błędu liczb rzeczywistych, gdy np. Musisz zwiększać jej wartość o ułamek w każdym obiegu pętli?
  4. Co się stanie, gdy wprowadzisz wartość 99999999999999999 do zmiennej typu float?
  5. Jak zadeklarujesz zmienną zmiennoprzecinkową bez znaku ze zwiększoną precyzją?

Dodano: 2018-02-23 10:34 przez Piotr Poźniak

ćwiczenia , zmienne , zmiennoprzecikowe , liczby rzeczywiste , float , double , long , unsigned , porównywanie ,
Piotr Poźniak
O autorze:

Programuję od ponad 15 lat. Prowadzę software house. Angażuję i zachęcam wszystkich do programowania w ramach inicjatywy Programowanie jest łatwe.