Zagadnienie liczb zmiennoprzecinkowych (ang. floating point numbers - FP) opisaliśmy szczegółowo w poprzednich rozdziałach. Tutaj zajmiemy się ich konkretną realizacją w systemie binarnym. Przypomnijmy, wartość liczby zmiennoprzecinkowej obliczamy według wzoru:
L(FP) = m x pc
gdzie:
L(FP) - wartość liczby zmiennoprzecinkowej
m - mantysa
p - podstawa
c - cechaW systemie dwójkowym wszystkie trzy elementy m, p i c będą zapisane dwójkowo za pomocą odpowiednio dobranego systemu kodowania liczb. Podstawa p zawsze będzie równa 2, zatem wzór obliczeniowy przyjmie postać:
L(FP) = m x 2c
Jak widać, do określenia formatu zmiennoprzecinkowego pozostaje nam podanie sposobu kodowania mantysy i cechy, ponieważ podstawa p=2 będzie zawsze znana.
Na potrzeby tego artykułu zdefiniujemy bardzo prosty dwójkowy system zmiennoprzecinkowy. Pozwoli nam on w zrozumiały sposób przedstawić wszystkie podstawowe właściwości rzeczywistych systemów zmiennoprzecinkowych, które dla zaawansowanych czytelników opisujemy szczegółowo w następnym rozdziale.
Słowo kodowe FP będzie zbudowane z 8 bitów ponumerowanych od 0 do 7, które podzielimy na dwie części:
Słowo kodowe FP b7 b6 b5 b4 b3 b2 b1 b0 cecha mantysa Cecha zawarta będzie w bitach od b4 do b7. Ustalmy, iż cecha jest liczbą całkowitą ze znakiem w kodzie U2 (Uwaga - ten sposób kodowania przyjęliśmy dla prostoty - rzeczywiste systemy zmiennoprzecinkowe stosują tutaj kodowanie z nadmiarem). Wartość cechy obliczamy wg wzoru:
c = b7(-23) + b622 + b521 + b420 = (-8)b7 + 4b6 + 2b5 + b4
W słówku kodowym 10111101(FP) cecha ma wartość 1011(U2) = -8 + 2 + 1 = -5(10).
Mantysa zawarta jest w bitach od b0 do b3. Ustalamy, iż mantysa jest 4-bitową liczbą stałoprzecinkową w kodzie U2 (rzeczywiste systemy stosują kodowanie mantysy w stałoprzecinkowym kodzie ZM) Pozycję przecinka umieszczamy pomiędzy bitami b1 i b2 (jest to tylko taka nasza umowa, w rzeczywistości przecinek nie jest umieszczany w zapisie liczby, ale my wiemy, gdzie on powinien być i o to właśnie chodzi!). Zatem wartość mantysy obliczamy według wzoru:
m = b3b2 , b1b0(U2) = b3(-21) + b220 + b12-1 + b02-2 = -2b3 + b2 + 1/2b1 + 1/4b2
W słówku kodowym 10111101(FP) mantysa ma wartość 11,01(U2) = -2 + 1 + 1/4 = -3/4(10).
Znając sposób kodowania cechy i mantysy możemy już obliczyć wartość dowolnej liczby zapisanej w tym systemie zmiennoprzecinkowym. W tym celu należy ze słowa kodu wydobyć bity cechy i mantysy. Na podstawie podanych definicji z wydobytych bitów uzyskujemy wartości cechy i mantysy, a następnie obliczamy wartość liczby zmiennoprzecinkowej zgodnie ze wzorem
L(FP) = m x 2c
Obliczyć wartość liczby zmiennoprzecinkowej 00010100(FP).
c = 0001(U2)
0001(U2) = 1(10)m = 01,00(U2)
01,00(U2) = 1(10)L(FP) = m x 2c = 1 x 21 = 1 x 2 = 2
00010100(FP) = 2
Obliczyć wartość liczby zmiennoprzecinkowej 11010111(FP).
c = 1101(U2)
1101(U2) = -8 + 4 + 1 = -3m = 01,11(U2)
01,11(U2) = 1 + 1/2 + 1/4 = 13/4L(FP) = m x 2c = 13/4 x 2-3 = 7/4 x 1/8 = 7/32
11010111(FP) = 7/32
Obliczyć wartość liczby zmiennoprzecinkowej 11111001(FP).
c = 1111(U2)
1111(U2) = -8 + 4 + 2 + 1 = -1(10)m = 10,01(U2)
10,01(U2) = -2 + 1/4 = -13/4L(FP) = m x 2c = -13/4 x 2-1 = -7/4 x 1/2 = -7/8
11111001(FP) = -7/8Z podanych powyżej przykładów widzimy jasno, iż nasz system pozwala kodować liczby ułamkowe zarówno dodatnie jak i ujemne.
|
Wzór L(FP) = m x 2c przyjmuje wartość maksymalną dla maksymalnej cechy i maksymalnej mantysy. Cecha przyjmie wartość maksymalną dla kodu:
0111(U2) = 7(10)
Mantysa największą wartość przyjmie dla kodu:
01,11(U2) = 13/4 = 7/4
Zatem max(FP) = 7/4 x 27 = 7/4 x 128 = 7 x 32 = 224
Wartość najmniejszą uzyskamy dla maksymalnej cechy i minimalnej mantysy. Cechę maksymalną policzyliśmy już powyżej. Minimalną mantysę reprezentuje kod:
10,00(U2) = -2
Zatem min(FP) = -2 x 27 = -2 x 128 = -256
Stąd wszystkie liczby reprezentowane przez nasz kod zawierają się w przedziale:
Z(FP) = -256 ... 224
|
W poniższej tabeli zebraliśmy wszystkie możliwe wartości, które może reprezentować nasz kod zmiennoprzecinkowy. Po lewej stronie mamy wszystkie możliwe cechy. U góry mamy wszystkie możliwe mantysy. Wartość liczby dla danej cechy i mantysy otrzymujemy na przecięciu wiersza cechy z kolumną mantysy.
mantysa | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 | ||
c e c h a | 0000 | 0 | 1/4 | 1/2 | 3/4 | 1 | 11/4 | 11/2 | 13/4 | -2 | -13/4 | -11/2 | -11/4 | -1 | -3/4 | -1/2 | -1/4 |
0001 | 0 | 1/2 | 1 | 11/2 | 2 | 21/2 | 3 | 31/2 | -4 | -31/2 | -3 | -21/2 | -2 | -11/2 | -1 | -1/2 | |
0010 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | -8 | -7 | -6 | -5 | -4 | -3 | -2 | -1 | |
0011 | 0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | -16 | -14 | -12 | -10 | -8 | -6 | -4 | -2 | |
0100 | 0 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | -32 | -28 | -24 | -20 | -16 | -12 | -8 | -4 | |
0101 | 0 | 8 | 16 | 24 | 32 | 40 | 48 | 56 | -64 | -56 | -48 | -40 | -32 | -24 | -16 | -8 | |
0110 | 0 | 16 | 32 | 48 | 64 | 80 | 96 | 112 | -128 | -112 | -96 | -80 | -64 | -48 | -32 | -16 | |
0111 | 0 | 32 | 64 | 96 | 128 | 160 | 192 | 224 | -256 | -224 | -196 | -180 | -128 | -96 | -64 | -32 | |
1000 | 0 | 1/1024 | 1/512 | 3/1024 | 1/256 | 5/1024 | 3/512 | 7/1024 | -1/128 | -7/1024 | -3/512 | -5/1024 | -1/256 | -3/1024 | -1/512 | -1/1024 | |
1001 | 0 | 1/512 | 1/256 | 3/512 | 1/128 | 5/512 | 3/256 | 7/512 | -1/64 | -7/512 | -3/256 | -5/512 | -1/128 | -3/512 | -1/256 | -1/512 | |
1010 | 0 | 1/256 | 1/128 | 3/256 | 1/64 | 5/256 | 3/128 | 7/256 | -1/32 | -7/256 | -3/128 | -5/256 | -1/64 | -3/256 | -1/128 | -1/256 | |
1011 | 0 | 1/128 | 1/64 | 3/128 | 1/32 | 5/128 | 3/64 | 7/128 | -1/16 | -7/128 | -3/64 | -5/128 | -1/32 | -3/128 | -1/64 | -1/128 | |
1100 | 0 | 1/64 | 1/32 | 3/64 | 1/16 | 5/64 | 3/32 | 7/64 | -1/8 | -7/64 | -3/32 | -5/64 | -1/16 | -3/64 | -1/32 | -1/64 | |
1101 | 0 | 1/32 | 1/16 | 3/32 | 1/8 | 5/32 | 3/16 | 7/32 | -1/4 | -7/32 | -3/16 | -5/32 | -1/8 | -3/32 | -1/16 | -1/32 | |
1110 | 0 | 1/16 | 1/8 | 3/16 | 1/4 | 5/16 | 3/8 | 7/16 | -1/2 | -7/16 | -3/8 | -5/16 | -1/4 | -3/16 | -1/8 | -1/16 | |
1111 | 0 | 1/8 | 1/4 | 3/8 | 1/2 | 5/8 | 3/4 | 7/8 | -1 | -7/8 | -3/4 | -5/8 | -1/2 | -3/8 | -1/4 | -1/8 |
Zwróć uwagę, iż pewne liczby występują wielokrotnie (0, 1, 2, 4), a pewnych liczb tutaj nie ma (9, 11, 13, 15). A przecież leżą one w wyliczonym wcześniej zakresie od -256 do 224. Dlaczego więc ich brakuje?
Aby to zrozumieć, spróbujmy zakodować liczbę 9 w naszym systemie zmiennoprzecinkowym. W tym celu musimy wyznaczyć cechę i mantysę. Cecha musi się zawierać w zakresie od -8 do 7, a mantysa w zakresie od -2 do 13/4. Początkowo przyjmiemy mantysę równą 9, a cechę równą 0. Następnie mantysę sprowadzimy do pożądanego zakresu dzieląc ją przez 2. Aby liczba zachowała po takim podziale swoją wartość, cecha musi być zwiększona o 1:
9 = 9 x 20 = 41/2 x 21 = 21/4 x 22 = 11/8 x 23
Otrzymaliśmy c = 3 oraz m = 11/8. Przeliczamy otrzymane wartości na kod U2:
3(10) = 0011(U2)
11/8 = 01,001(U2).
Zwróć uwagę, iż mantysa jest o 1 bit za długa - ma 5 bitów, a w naszej liczbie zmiennoprzecinkowej możemy zapamiętać tylko 4 bity mantysy. Życie jest okrutne, skoro nasz format wymaga 4 bitów, musimy odrzucić ostatni bit mantysy.
Zatem do kodu powędruje wartość 01,00(U2) zamiast wyliczonej 01,001(U2). Otrzymamy kod 00110100(FP). Szukamy w naszej tabelce i znajdujemy:
00110100(FP) = 8
Zatem z wartości 9 zrobiło się nam 8. Dlaczego? Po prostu wystąpiło zjawisko zwane utratą precyzji lub błędem zaokrąglenia. Liczba 9 wymaga większej precyzji niż oferuje nasz system i nie może w nim zostać przedstawiona dokładnie - zostanie zaokrąglona do 8.
| |||
program precyzja;
var
x : single; // 32-bitowa zmienna fp
begin
x := 100000001; // tego zmienna nie zapamięta dokładnie!
writeln(x:0:0); // ups, utrata precyzji !!!
readln;
end.Sprawdź wyniki pracy tego prostego programu. Czy teraz cię to dziwi?
| |||
Dla liczb dodatnich ilość bitów znaczących uzyskamy usuwając wszystkie początkowe i końcowe zera. To, co pozostanie, jest właśnie bitami znaczącymi.
Kod 00101100 ma 4 bity znaczące 1011.
Kod 00010100 ma 3 bity znaczące 101.
Dla liczb ujemnych usuwamy początkowe jedynki i końcowe zera.
Kod 11101000 ma 2 bity znaczące 01.
Kod 11110010 ma 3 bity znaczące 001.
Po tych ustaleniach już łatwo możemy określić, czy liczba da się zapisać w danym systemie zmiennoprzecinkowym. W tym celu wystarczy przeliczyć ją na system dwójkowy i obliczyć ilość bitów znaczących. Jeśli mantysa może przechować te bity, to liczba będzie przedstawiona dokładnie. Jeśli nie, mamy utratę precyzji zapisu - liczba będzie zapamiętana z zaokrągleniem.
| |||
|
Dodawanie i odejmowanie
Wbrew pozorom zasady arytmetyki zmiennoprzecinkowej nie są wcale trudne - jak zwykle diabeł tkwi w szczegółach. Mamy dane dwie liczby zmiennoprzecinkowe:
gdzie
L1, L2 - wartości liczb
m1, m2 - mantysy
c1, c2 - cechySuma lub różnica tych dwóch liczb wynosi:
Z wyliczeń tych wynika, iż mantysa sumy (lub różnicy) jest sumą (lub różnicą) mantys liczb wyjściowych po sprowadzeniu ich do wspólnej cechy (operacja ta nosi nazwę wyrównania cech liczb zmiennoprzecinkowych). Cecha sumy (lub różnicy) jest równa sumie cech dodawanych (lub odejmowanych) liczb. Po wykonaniu operacji arytmetycznej mantysa wyniku jest sprowadzana do postaci znormalizowanej i zapamiętywana w kodzie liczby zmiennoprzecinkowej.
W systemie dwójkowym operacja dzielenia przez 2 jest równoważna przesunięciu wszystkich bitów zapisu liczby o jedną pozycję w prawo (tak jak w systemie dziesiętnym podział przez 10). Z kolei mnożenie przez 2 odpowiada przesunięciu wszystkich cyfr o jedną pozycję w lewo.
Dzielenie lub mnożenie przez potęgi liczby 2 jest zatem przesuwaniem bitów o odpowiednią ilość pozycji (równą wykładnikowi potęgi liczby 2) w prawo (dzielenie) lub w lewo (mnożenie). Obie operacje są bardzo proste i nie wymagają wykonywania żadnych działań arytmetycznych (w procesorze realizują je układy zwane rejestrami przesuwnymi - ang. shift registers).
Obliczyć 11110100(FP) + 11100110(FP).
Z zapisu zmiennoprzecinkowego wydobywamy cechy i mantysy obu liczb:
c1 = 1111(U2) = -1; m1 = 01,00(U2)
c2 = 1110(U2) = -2; m2 = 01,10(U2)Pierwszą mantysę musimy podzielić przez 2-2, co odpowiada mnożeniu przez 22. Zatem wszystkie jej bity przesuwamy o 2 pozycje w lewo:
m1 = 0100,00(U2)
Drugą mantysę musimy podzielić przez 2-1, co odpowiada mnożeniu przez 21. Wszystkie jej bity przesuwamy o 1 pozycję w lewo:
m2 = 011,00(U2)
Obliczamy mantysę sumy:
0100,00 + 0011,00 0111,00 m1 + m2 = 0111,00
Obliczamy cechę sumy:
1111 + 1110 1101 Mantysę sumy sprowadzamy do postaci znormalizowanej:
c = 1101(U2) ; m = 0111,00(U2) - za duża, przesuwamy o 1 bit w prawo i zwiększamy cechę o 1
c = 1110(U2) ; m = 0011,10(U2) - za duża, powtarzamy przesuw i zwiększanie cechy
c = 1111(U2) ; m = 0001,11(U2) - mantysa w zakresie, koniec operacjiOtrzymaną cechę i mantysą łączymy w jeden kod i otrzymujemy wynik operacji dodawania:
11110100(FP) + 11100110(FP) = 11110111(FP).
Sprawdźmy, czy wynik jest prawidłowy. W tym celu posługując się tabelką wyznaczamy wartości poszczególnych liczb zmiennoprzecinkowych:
11110100(FP) = 1/2
11100110(FP) = 3/8
11110111(FP) = 7/81/2 + 3/8 = 4/8 + 3/8 = 7/8 - wynik prawidłowy.
A teraz spróbujmy zsumować liczbę dużą i małą: 01000110(FP) + 00010110(FP) (24 + 3):
Wydobywamy cechy i mantysy:
c1 = 0100(U2) = 4; m1 = 01,10(U2)
c2 = 0001(U2) = 1; m2 = 01,10(U2)Pierwszą mantysę przesuwamy o 1 bit w prawo, a drugą o 4 bity w prawo:
m1 = 00,11000(U2)
m2 = 00,00011(U2)Sumujemy cechy i mantysy:
m1 + m2 = 00,11011(U2) ; c1 + c2 = 0101(U2)
Normalizujemy mantysę wyniku:
c = 0101(U2) ; m = 00,11011(U2)
c = 0100(U2) ; m = 01,10110(U2)Łączymy otrzymaną cechę i mantysę w jeden kod zmiennoprzecinkowy otrzymując wynik dodawania:
01000110(FP) + 00010110(FP) = 01000110(FP) = 24(10).
Zwróć uwagę, iż suma jest równa pierwszej z sumowanych liczb. Zatem dodanie drugiej liczby nie wpłynęło na wynik sumowania. Nastąpiła utrata precyzji. Wynika stąd bardzo ważny wniosek:
| |||
Aby się przekonać, iż kolejność sumowania jest bardzo istotna, uruchom poniższy program i sprawdź jego wynik - w obu przypadkach sumuje on te same liczby, jednak raz od najmniejszej do największej, a za drugim razem od największej do najmniejszej. Czy otrzymujemy ten sam wynik sumowania?
program precyzja;
var
s : single;
i : integer;
begin
s := 0;
for i := 1 to 100000000 do s := s + i/10000;
writeln(s:0:0);
s := 0;
for i := 100000000 downto 1 do s := s + i/10000;
writeln(s:0:0);
readln;
end.
Dzielenie i mnożenie
Teraz wyprowadzimy reguły mnożenia i dzielenia liczb zmiennoprzecinkowych. Mamy dane dwie liczby:
gdzie
L1, L2 - wartości liczb
m1, m2 - mantysy
c1, c2 - cechyWykonując proste przekształcenia algebraiczne otrzymujemy wzory na iloczyn i iloraz tych dwóch liczb:
Wynika z nich, iż mantysa iloczynu jest iloczynem mantys, cecha iloczynu jest sumą cech. Mantysa ilorazu jest ilorazem mantys, a cecha ilorazu jest różnicą cech. Po wykonaniu operacji arytmetycznej mantysę wynikową normalizujemy i łączymy z cechą otrzymując gotowy kod zmiennoprzecinkowy. Co ciekawe, operacje mnożenia i dzielenia są koncepcyjnie prostsze w systemie zmiennoprzecinkowym od operacji dodawania i odejmowania.
Sprawdzenie poprawności tych wzorów pozostawiam ambitnym czytelnikom jako zadanie domowe.
Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.
Źródło: mgr Jerzy Wałaszek