Cóż, operacja 0.1 - 0.1, tudzież 0.1 + (-0.1) zawsze da w wyniku dokładnie 0. Jednak w wyrażeniu 0.1 + 0.1 + 0.1 - 0.1 - 0.1 - 0.1 dzieje się coś innego. Co takiego?
Prosty program w C++ pozwala sprawdzić wartość bitową danej liczby zmiennoprzecinkowej. Oto jego źródło:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
struct fl
{
union
{
float val;
unsigned int bit;
};
string print() const
{
ostringstream os;
// sign
os << (bit >> 31) << " ";
// exponent
for (int i = 0; i < 8; ++i)
os << ((bit >> (30 - i)) & 1);
os << " ";
// mantissa
for (int i = 0; i < 23; ++i)
os << ((bit >> (22 - i)) & 1);
// value
os << " [" << (*(float*)&val) << "]";
return os.str();
}
};
int main()
{
fl x;
x.val = 0.1f;
cout << x.print() << endl;
x.val += 0.1f;
cout << x.print() << endl;
x.val += 0.1f;
cout << x.print() << endl;
x.val -= 0.1f;
cout << x.print() << endl;
x.val -= 0.1f;
cout << x.print() << endl;
x.val -= 0.1f;
cout << x.print() << endl;
return 0;
}
Program ten krok po kroku oblicza wspomniane wyrażenie. Startuje od wartości 0.1, potem dodaje dwukrotnie 0.1, a następnie trzykrotnie odejmuje. W wynikach programu można zauważyć, co dzieje się z mantysą liczby:
0 01111011 10011001100110011001101 [0.1]
0 01111100 10011001100110011001101 [0.2]
0 01111101 00110011001100110011010 [0.3]
0 01111100 10011001100110011001110 [0.2]
0 01111011 10011001100110011001111 [0.1]
0 01100101 00000000000000000000000 [1.49012e-08]
Ważna uwaga Wszelkie operacje wewnątrz koprocesora wykonywane są z maksymalną możliwą precyzją. Często jest to precyzja 80-bitowa! Dopiero po wykonaniu działania, wyliczona wartość jest konwertowana do precyzji takiej, jaka jest żądana.
Można prześledzić, co się dzieje. Jak widać, na najmniej znaczącym bicie mantysy od początku mamy niedokładność, która zaburza okres 00110011001100... . Przy pierwszym dodaniu wartości 0.1, wartość mantysy się nie zmieniła. Dlaczego? bo dla 0.1 + 0.1 błąd jest zbyt mały i w wyniku zaokrąglenia (ucięcia) mantysy do długości 23 bitów został zniwelowany.
Jednak przy drugim dodawaniu mantysa psuje się. W ogóle, operacje dodawania i odejmowania liczb zmiennoprzecinkowych są obarczone największym błędem, szczególnie wtedy, gdy ich cechy są różne (tak jak teraz). Jest tak dlatego, że aby liczby mogły zostać dodane lub odjęte, muszą zostać
- sprowadzone do wspólnej, większej cechy (utrata precyzji) oraz
- zdenormalizowane.
Oznacza to tyle, po sprowadzeniu do wspólnej cechy, mantysa obu operandów jest przesuwana bitowo w prawo o 2, a cecha zwiększana o wartość 2. Po co? Aby "wciągnąć" do mantysy ten domyślny bit jedności, oraz po jego lewej stronie zostawić dodatkowe miejsce na ewentualne przeniesienie. Może lepiej będzie to widoczne na przykładzie (mocno uproszczonym, z mantysą == 1):
x = 0 01111100 00000000000000000000000 [0.125]
y = 0 01111110 00000000000000000000000 [0.5]
Sumowanie
1) Sprowadzenie do wspólnej cechy
Większą cechę ma liczba y, więc do takiej wartości cechy sprowadzany jest x (co już jest denormalizacją):
x = 0 01111110 01000000000000000000000 [0.125]
2) Denormalizacja
Należy zwiększyć cechę y o 2, tak, aby "domyślny" bit znajdował się w mantysie i przed nim było jeszcze miejsce na ewentualne przeniesienie:
y = 0 10000000 01000000000000000000000 [0.5]
Wciąż musimy zachować zgodność cechy w obu liczbach, więc również o 2 zwiększamy cechę x:
x = 0 10000000 000100000000000000000 [0.125]
Teraz w normalny sposób dodajemy do siebie mantysy:
res = 0 10000000 010100000000000000000 [0.625]
Teraz następuje normalizacja wyniku -- to znaczy przesuwamy mantysę o tyle bitów w lewo, aż utniemy pierwszy zapalony bit, a mantysę zmniejszamy o wartość równą przesunięciu. W tym przypadku przesuwamy w lewo o 2 bity i cechę zmniejszamy o 2:
res = 0 01111110 01000000000000000000000 [0.625]
Wszystkie te operacje, które pokazałem, nie są możliwe do zauważenia -- denormalizacja etc. zachodzi wewnątrz koprocesora arytmetycznego.
I teraz: dlaczego dodawanie i odejmowanie powodują tak duże błędy zaokrąglenia? Zauważmy, że ostatni bit mantysy jest zaokrągleniem powodującym błąd -- rzeczywista wartość liczby jest nieco większa od wartości 0.1. Przy kolejnych dodawaniach ten błąd się powiększa i propaguje na bardziej znaczące bity mantysy. Po drugim dodawaniu mantysa jest zaburzona, i to na bardziej znaczącym miejscu niż dla wartości wyjściowej. Teraz operacje odejmowania będą propagować ten błąd dalej.
// EDIT
Teraz przyjrzałem się temu, co napisałem, i obawiam się, że mogłem trochę skomplikować. Dygresje kiedyś się na mnie zemszczą. ;) Jeśli wciąż jest to niejasne, oto wyjaśnienie w pigułce: rzeczywista wartość jest odrobinę większa od 0.1 ze względu na zaokrąglenie w górę ostatniego bitu mantysy. Podczas dodawania ten błąd również się dodaje. Dlaczego odejmowanie nie niweluje go? Ponieważ odejmowanie działa odrobinę inaczej, zasady przenoszenia i zaokrąglania są trochę inne. Łatwo można sprawdzić, że wyrażenie 0.1 + 0.1 + 0.1 - (0.1 + 0.1 + 0.1) jest dokładnie równe 0.
// EDIT 2
0.1 to będzie 0 01111011 10011001100110011001100 (tylko tu mam wątpliwości, czy mantysę uciąć tak jak jest, czy przybliżyć do 10011001100110011001101?)
Mantysa zawsze jest zaokrąglana do najbliższej wartości możliwej do reprezentacji -- w tym wypadku do 10011001100110011001101.