Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

Autor artykułu: mgr Jerzy Wałaszek

©2024 mgr Jerzy Wałaszek
I LO w Tarnowie

Przykładowy system zmiennoprzecinkowy

SPIS TREŚCI
Podrozdziały

Liczby zmiennoprzecinkowe

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 · pc

gdzie:

L(FP) - wartość liczby zmiennoprzecinkowej
m - mantysa
p - podstawa
c - cecha

W 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 · 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 początek:  podrozdziału   strony 

Definicja formatu zmiennoprzecinkowego

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) + b6 · 22 + b5 · 21 + b4 · 20
c = (-8) · b7 + 4 · b6 + 2 · b5 + b4

Przykład:

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)
m = b3 · (-21) + b2 · 20 + b1 · 2-1 + b0 · 2-2
m = -2b3 + b2 + 1/2b1 + 1/4b2

Przykład:

W słówku kodowym 10111101(FP) mantysa ma wartość

11,01(U2) = -2 + 1 + 1/4 = -3/4(10).

Na początek:  podrozdziału   strony 

Obliczanie wartości liczby zmiennoprzecinkowej

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 · 2c

Przykład:

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 · 2c = 1 · 21 = 1 · 2 = 2
00010100(FP) = 2

Przykład:

Obliczyć wartość liczby zmiennoprzecinkowej 11010111(FP).

c = 1101(U2)
1101(U2) = -8 + 4 + 1 = -3
    m = 01,11(U2)
01,11(U2) = 1 + 1/2 + 1/4 = 13/4

L(FP) = m · 2c = 13/4 · 2-3 = 7/4 · 1/8 = 7/32
11010111(FP) = 7/32

Przykład:

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/4

L(FP) = m · 2c = -13/4 · 2-1 = -7/4 · 1/2 = -7/8
11111001(FP) = -7/8

Z podanych powyżej przykładów widzimy jasno, iż nasz system pozwala kodować liczby ułamkowe zarówno dodatnie jak i ujemne.


Na początek:  podrozdziału   strony 

Zakres liczb zmiennoprzecinkowych

Wzór L(FP) = m · 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 · 27 = 7/4 · 128 = 7 · 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 · 27 = -2 · 128 = -256

Stąd wszystkie liczby reprezentowane przez nasz kod zawierają się w przedziale:

Z(FP) = -256 ... 224

Na początek:  podrozdziału   strony 

Precyzja liczb zmiennoprzecinkowych

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?

Przykład:

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 · 20 = 41/2 · 21 = 21/4 · 22 = 11/8 · 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.

UWAGA:

Jeśli myślisz, że jest to tylko cecha naszego niedoskonałego systemu zmiennoprzecinkowego, to jesteś w grubym błędzie. Zjawisko utraty precyzji występuje w nawet najdroższym procesorze PENTIUM IV i na razie nie można nic na to poradzić. Pewnych liczb po prostu nie da się przedstawić dokładnie. Oczywiście w rzeczywistym systemie zmiennoprzecinkowym mantysa zbudowana jest z dużo większej ilości bitów, zatem jej rozdzielczość jest o wiele większa od naszej.

C++
// Precyzja

#include <iostream>
#include <iomanip>

using namespace std;

main()
{
  float x;  // 32-bitowa zmienna fp
  char  z[1];

  cout << setprecision(0) << fixed;
  x = 100000001;     // tego zmienna nie zapamięta dokładnie!
  cout << x << endl; // ups, utrata precyzji !!!
  cout << "\nNacisnij ENTER...\n";
  cin.getline(z,1);
}
Pascal
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?

Zapamiętaj:

Liczba da się przedstawić dokładnie w danym systemie zmiennoprzecinkowym, jeśli leży w zakresie dozwolonych wartości oraz liczba jej bitów znaczących jest mniejsza lub równa ilości bitów mantysy bez bitu znakowego.

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.

Przykład:

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.

Przykład:

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.

Zapamiętaj:

Liczby zmiennoprzecinkowe przechowują wartości przybliżone - pewnych liczb nie da się zapisać w systemie zmiennoprzecinkowym. Typowym przykładem są ułamki dziesiętne typu 1/10, 2/10,3/10. Takie ułamki posiadają nieskończone rozwinięcia w systemie dwójkowym, zatem wymagają mantys o nieskończonej ilości bitów, co nie jest możliwe do zrealizowania.

Ilość bitów mantysy określa precyzję zapisu. Im jest większa, tym dokładniej można przedstawiać liczby rzeczywiste - błędy zaokrągleń są mniejsze.

Ilość bitów cechy określa dopuszczalny zakres reprezentowanych liczb.


Na początek:  podrozdziału   strony 

Arytmetyka liczb zmiennoprzecinkowych

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

L 1, L 2 - wartości liczb
m 1, m 2 - mantysy
c 1, c 2 - cechy

Suma 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).

Przykład:

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 operacji

Otrzymaną 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/8
1/2 + 3/8 = 4/8 + 3/8 = 7/8 - wynik prawidłowy.

Przykład:

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:

Uwaga, pułapka:

W systemie zmiennoprzecinkowym sumowanie liczby dużej z liczbą małą może być niedokładne z uwagi na utratę precyzji. Dlatego sumując ciąg liczb zawsze rozpoczynajmy sumowanie od wartości najmniejszych do największych.

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?

C++
// Precyzja

#include <iostream>
#include <iomanip>

using namespace std;

main()
{
  float s;
  int   i;
  char  z[1];

  cout << setprecision(0) << fixed;
  s = 0;
  for(i = 1; i <= 100000000; i++) s += i/10000;
  cout << s << endl;
  s = 0;
  for(i = 100000000; i >= 1; i--) s += i/10000;
  cout << s << endl;
  cout << "\nNacisnij ENTER...\n";
  cin.getline(z,1);
}
Pascal
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

L 1, L 2 - wartości liczb
m 1, m 2 - mantysy
c 1, c 2 - cechy

Wykonują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.


Na początek:  podrozdziału   strony 

Zadania

Zadanie 1 (łatwe)

Wyznacz wartość dziesiętną liczb zmiennoprzecinkowych:
00101111(FP) (10)  

.

01010010(FP) (10)  

.

00100100(FP) (10)  

.

01111111(FP) (10)  

.

01110110(FP) (10)  

.


Na początek:  podrozdziału   strony 

Zobacz dalej...
Standard IEEE 754 | Wywiad z twórcą standardu IEEE 754, Williamem Kahanem


Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2024 mgr Jerzy Wałaszek

Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.