13
Dodatek do Wykładu 01: Kodowanie liczb w komputerze [materiał ze strony: http://sigma.wsb-nlu.edu.pl/~szyszkin/] Wszelkie dane zapamiętywane przetwarzane przez komputery muszą być odpowiednio zakodowane. Z powodów technicznych najwygodniej jest realizować układy, w których są rozróżnialne dwa stany. Stany te będziemy dla wygody oznaczali symbolami 0 i 1. Tak więc w pamięci komputera znajdują się same ciągi zer i jedynek. Na ogół są one grupowane w pewne większe porcje: osiem bitów to bajt (zwany czasami oktetem), szesnaście bitów to pojedyncze słowo. Dokładna terminologia zależy przede wszystkim od architektury danego sytemu komputerowego (głównie chodzi o procesor i pamięć), ale bajt jest standardem. Tak więc w komputerze ośmiobitowym w pamięci mamy komórki, w których znajdują się porcje po osiem bitów (np. jedne z pierwszych procesorów firmy Intel (1974 r.) oznaczane jako 8008, 8080 były ośmiobitowe 1 ). Tak więc fragment pamięci możemy sobie wyobrazić następująco: 1 0 0 0 1 1 0 0 1 0 1 1 0 1 0 0 1 1 0 0 1 0 1 0 0 1 0 1 0 0 1 1 Z drugiej strony, aby efektywnie przetwarzać dane np. używając języków programowania do pisania odpowiednich programów potrzebujemy bardziej abstrakcyjnych pojęć takich, jak liczby całkowite, liczby wymierne (z „częścią po przecinku dziesiętnym”), znaki tekstowe. Wszystkie te obiekty muszą być jednak zapamiętane w postaci ciągów zer i jedynek. Aby to zrobić efektywnie należy umówić się co do sposobu kodowania tych obiektów. Jak reprezentować liczby 20, -31, 124.12, symbole ‘A’, ‘a’ czy ‘x’? Mimo, że od strony czysto matematycznej liczby wymierne są rozszerzeniem zbioru liczb całkowitych, to w technice komputerowej stosuje się zupełnie inne sposoby kodowania dla liczb całkowitych i liczb wymiernych. Stąd w większości języków programowania rozróżnia się generalnie typy całkowitoliczbowe (np. int, short w języku C/C++) od typów nie całkowitoliczbowych (np. float, double w języku C/C++). Ze względu na sposób reprezentacji używa się tutaj często określenia typy stałoprzecinkowe i typy zmiennoprzecinkowe. Reprezentacje liczb całkowitych Kodowanie NKB i U1 Podstawą jest tutaj tzw. naturalny kod binarny (NKB). Jest to reprezentacja analogiczna do używanej na co dzień, gdy posługujemy się liczbami całkowitymi, czyli zapis pozycyjnym o podstawie 10. Na przykład w układzie dziesiętnym zapis 24564 znaczy tyle co 1 Podstawowe rejestry tych dwóch procesorów były 8-bitowe, ale były one w stanie używać adresów 16- bitowych. Tak więc ich przestrzeń adresowa wynosiła 2 16 =64 K. Współpracowały one z 8-bitową szyną danych, dlatego najbardziej naturalną pamięcią była taka, w której organizacja była 8-bitowa.

Dodatek do Wykładu 01: Kodowanie liczb w komputerze14mstygar/files/kodowanie-liczb... · 10. Na przykład w układzie dziesiętnym zapis 24564 znaczy tyle co. 1 Podstawowe rejestry

  • Upload
    doanbao

  • View
    236

  • Download
    0

Embed Size (px)

Citation preview

Dodatek do Wykładu 01: Kodowanie liczb w komputerze [materiał ze strony: http://sigma.wsb-nlu.edu.pl/~szyszkin/]

Wszelkie dane zapamiętywane przetwarzane przez komputery muszą być odpowiednio zakodowane. Z powodów technicznych najwygodniej jest realizować układy, w których są rozróżnialne dwa stany. Stany te będziemy dla wygody oznaczali symbolami 0 i 1. Tak więc w pamięci komputera znajdują się same ciągi zer i jedynek. Na ogół są one grupowane w pewne większe porcje: osiem bitów to bajt (zwany czasami oktetem), szesnaście bitów to pojedyncze słowo. Dokładna terminologia zależy przede wszystkim od architektury danego sytemu komputerowego (głównie chodzi o procesor i pamięć), ale bajt jest standardem. Tak więc w komputerze ośmiobitowym w pamięci mamy komórki, w których znajdują się porcje po osiem bitów (np. jedne z pierwszych procesorów firmy Intel (1974 r.) – oznaczane jako 8008, 8080 – były ośmiobitowe1). Tak więc fragment pamięci możemy sobie wyobrazić następująco:

1 0 0 0 1 1 0 0 1 0 1 1 0 1 0 0 1 1 0 0 1 0 1 0 0 1 0 1 0 0 1 1

Z drugiej strony, aby efektywnie przetwarzać dane – np. używając języków programowania do pisania odpowiednich programów – potrzebujemy bardziej abstrakcyjnych pojęć takich, jak liczby całkowite, liczby wymierne (z „częścią po przecinku dziesiętnym”), znaki tekstowe. Wszystkie te obiekty muszą być jednak zapamiętane w postaci ciągów zer i jedynek.

Aby to zrobić efektywnie należy umówić się co do sposobu kodowania tych obiektów. Jak reprezentować liczby 20, -31, 124.12, symbole ‘A’, ‘a’ czy ‘x’?

Mimo, że od strony czysto matematycznej liczby wymierne są rozszerzeniem zbioru liczb całkowitych, to w technice komputerowej stosuje się zupełnie inne sposoby kodowania dla liczb całkowitych i liczb wymiernych. Stąd w większości języków programowania rozróżnia się generalnie typy całkowitoliczbowe (np. int, short w języku C/C++) od typów nie całkowitoliczbowych (np. float, double w języku C/C++). Ze względu na sposób reprezentacji używa się tutaj często określenia typy stałoprzecinkowe i typy zmiennoprzecinkowe.

Reprezentacje liczb całkowitych

Kodowanie NKB i U1

Podstawą jest tutaj tzw. naturalny kod binarny (NKB). Jest to reprezentacja analogiczna do używanej na co dzień, gdy posługujemy się liczbami całkowitymi, czyli zapis pozycyjnym o podstawie 10. Na przykład w układzie dziesiętnym zapis 24564 znaczy tyle co

1 Podstawowe rejestry tych dwóch procesorów były 8-bitowe, ale były one w stanie używać adresów 16-bitowych. Tak więc ich przestrzeń adresowa wynosiła 216=64 K. Współpracowały one z 8-bitową szyną danych, dlatego najbardziej naturalną pamięcią była taka, w której organizacja była 8-bitowa.

4 3 2 1 024564 2 10 4 10 5 10 6 10 4 10 .= ⋅ + ⋅ + ⋅ + ⋅ + ⋅

Do takiego kodowania potrzebujemy dziesięciu symboli (cyfr) 0,1,2,3,4,5,6,7,8,9 co nie jest

praktyczne przy realizacji sprzętowej, tak więc używamy podstawy 2, co wymaga tylko dwóch symboli 0, 1. Oznacza to, że np. ciąg bitów 10100110 interpretujemy jako 102, gdyż:

0 1 2 3 4 5 6 72(01100110) 0 2 1 2 1 2 0 2 0 2 1 2 1 2 0 2 102.= ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ =

Problem pojawi się gdy będziemy chcieli reprezentować liczby całkowite ze znakiem. Jak zapisać tylko przy pomocy 0 i 1 liczbę –102? Jednym z rozwiązań jest wprowadzenie tzw. bitu znaku: pierwszy bit (licząc od lewej strony), nie oznacza wagi, ale informuje o znaku: 1 oznacza – (minus), a 0 oznacza + (plus). Takie kodowanie nazywamy kodem uzupełnień do jeden (U1). Tak więc: ciąg 10011001 oznacza:

• w kodzie U1: -25

• w kodzie NKB: 153

Zauważmy, że w kodze U1 mamy dwie reprezentacje zera, czyli dwa różna ciągi należy interpretować jako jedno matematyczne 0:

• 10000000 = -0 = 0

• 00000000 = +0 = 0

Kod U1 mimo, że bardzo prosty i oczywisty nie jest jednak używany w praktyce. Wynika to przede wszystkim z tego, że bardzo niewygodnie realizuje się w nim operacje dodawania czy mnożenia. Nie wystarczy zwykła suma bitowa z przeniesieniem.

Przykład 1

1 0 1 1 0 0 0 0 0 0 0 1 0 0 1 1 1 1 0 0 0 0 1 1

Jak widać zwykłe dodawanie bitowe z przeniesieniem prowadzi do niepoprawnych wyników. Oznacza to, że do obsługi liczb całkowitych ze znakiem kodowanych w systemie U1 należałoby wykorzystać bardziej skomplikowane algorytmy (zrealizowane sprzętowo lub programowo).

Kodowanie U2

Zakładamy, że używamy zawsze porcji (ciągów) bitów o długości n (w praktyce komputerowej jest to 8, 16, 32, 64 czy 128).

Ciąg zer i jedynek o długości n interpretujemy w kodzie U2 jako następującą wartość:

1 2 1 0

1 2 1 0 2 1 2 1 0( ... ) 2 2 ... 2 2 .n nn n U n nx x x x x x x x− −− − − −= − ⋅ + ⋅ + + ⋅ + ⋅

-48

19

-67

Tak więc mamy

2 1 0 2 11 2 1 0 2 1

2 1 0 2 1

( ) gdy 0,( ... )

2 ( ) gdy 1.n n

n n U nn n

x x x xx x x x

x x x x− −

− − −− −

== − + =

(0.1)

Przykład 2

Dysponujemy maszyną ośmiobitową. Naturalne jest więc używanie kodu U2 z 8.n = Jakim liczbom całkowitym odpowiadają następujące sekwencje znajdujące się w pamięci?

1 1 0 1 1 0 0 1 0 0 0 0 1 1 1 0 0 1 1 0 0 0 1 0 1 0 1 1 1 0 0 1 1 1 0 1 1 0 1 0

Dla pierwszego ciągu mamy 7 6 5 4 3 2 1 0

2(11011001) 2 1 2 0 2 1 2 1 2 0 2 0 2 1 2128 64 16 8 1 39.

U = − + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ == − + + + + = −

a dla drugiego ciągu 7 6 5 4 3 2 1 0

2(00001110) 0 2 0 2 0 2 0 2 1 2 1 2 1 2 0 28 4 2 14.

U = − ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ == + + =

Jaka jest największa liczba dodatnia, którą można zapisać używając kodu U2 dla n=8 lub n=16?

Aby uzyskać liczbę dodatnią pierwszy (od lewej) bit musi być 0. Jeżeli pozostałe będą równe 1, to uzyskamy

6 5 4 3 2 1 02

214 13 12 11 10 8 7 6 5 4 3 2 1 0

(01111111) 2 2 2 2 2 2 2 127.(0111111111111111)

2 2 2 2 2 2 2 2 2 2 2 2 2 2 32767

U

U

= + + + + + + ==

= + + + + + + + + + + + + + =

Ogólnie dla reprezentacji n bitowej największa liczba w kodzie U2 to 1

2(011111 111) 2 1.nU

−= − Jaka jest najbardziej ujemna liczba, którą można zapisać używając kodu U2 dla n=8 lub n=16?

Liczby ujemne w kodzie U2 mają 1 na pierwszej (od lewej) pozycji. Aby uzyskać liczbę ujemna o największej wartości bezwzględnej, widzimy ze wzoru (0.1), że pozostałe bity muszą być równe 0. Tak więc

7 6 5 4 3 2 1 02(10000000) 2 0 2 0 2 0 2 0 2 0 2 0 2 0 2 128.U = − + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ = −

Analogicznie dla kodu 16-bitowego U2 mamy 15 16 0

2(1000000000000000) 2 0 2 0 2 32768.U = − + ⋅ + + ⋅ = − Ogólnie dla kodów o długości n w reprezentacji U2 „najbardziej” ujemna liczba to

12(10000 000) 2 .n

U−= −

Ile różnych liczb całkowitych można zapisać używając kodu U1 i U2 dla n=8 lub n=16?

Z powyższych przykładów widać, że podzbiory liczb całkowitych, kodowane metodą U2 są następujące. Dla 8n = to

{ 128, , 1,0,1, ,127},− − a dla n=16 to { 32768, , 1,0,1, 32767}.− −

Ogólnie dla reprezentacji n bitowej mamy zakres

1 1{ 2 , , 2 1}.n n− −− − Liczba zakodowanych wartości wynosi oczywiście 2 .n Zalety kodu U2:

• jednoznaczna reprezentacja liczby 0

• łatwa realizacja dodawania i odejmowania

Dodawanie dwóch liczb zapisanych w kodzie U2 można wykonać wykonując zwykłe dodawanie

binarne z przeniesieniem. Tak długo jak nie ma przepełnienia wyniki wychodzą poprawne.

Przykład

Poniżej są pokazane liczby w pamięci 8-bitowej, które interpretujemy jak liczby całkowite zapisane w kodzie U2. Wykonajmy dodawanie (dwa pierwsze rzędy) metodą bitową z przenoszeniem, aby przekonać się, że wyniki są poprawne.

1 0 0 1 0 0 1 1 0 1 0 0 0 1 0 1 1 1 0 1 1 0 0 0

Przykład

Wykonać dodawanie następujących par liczb po zakodowaniu ich w kodzie U2:

a) x = -25, y = 80

b) x = -68, y = 92

c) x = -37, y = 37

Odejmowanie liczb jest oczywiście szczególnym przypadkiem dodawania. Warto jednak omówić osobno to działanie, aby zobaczyć jak prosto w kodzie U2 można uzyskać liczbę przeciwną do danej.

Jeżeli mamy zakodowaną liczbę 1 1{ 2 , , 2 1}n nx − −∈ − − ⊂

2 1 2 0 2( ) ( , , ) ,U n n Ux x x x− −= (0.2)

to liczbę przeciwną, ,x− uzyskujemy przez negację bitową reprezentacji wyjściowej i dodanie bitowe jedynki. Tak więc reprezentację w kodzie U2 liczby ujemnej możemy uzyskać następującym wzorem

-109

69

-40

-109+69=-40

( )2 2( ) | | 1,Ux x− = +

gdzie kreska oznacza negację bitową, czyli 1 0= oraz 0 1.=

Przykład

Zakładamy, że liczby są reprezentowane ośmiobitowo ( 8).n = Chcemy podać reprezentację U2 liczby 42.−

Obliczamy reprezentację liczby | |42 42 :x = − =

0 0 1 0 1 0 1 0

Dokonujemy zaprzeczenia bitowego powyższego kodu, zatem (| |) :x

1 1 0 1 0 1 0 1

Dodajemy bitowo jedynkę, czyli obliczamy ( )2| | 1:x +

1 1 0 1 0 1 1 0 Sprawdzamy:

7 6 4 2 12(11010110) 2 2 2 2 2 128 64 16 4 2 128 86 42.U = − + + + + = − + + + + = − + = −

Jeszcze jeden przykład, liczba 108.−

| |108 108 :− =

0 1 1 0 1 1 0 0

(| 108|) :−

1 0 0 1 0 0 1 1

(| 108|) 1:− +

1 0 0 1 0 1 0 0 Sprawdzamy:

7 4 22(10010100) 2 2 2 128 16 4 108.U = − + + = − + + = −

Zauważmy, że w kodzie U2 powielenie najstarszego (pierwszego od lewej) bitu nie zmienia

wartości liczby. Na przykład powielanego bitu 1 mamy 4 2 1

25 4 2 1

26 5 4 2 1

2

(10110) 2 2 2 16 6 10,

(110110) 2 2 2 2 32 16 4 2 10

(1110110) 2 2 2 2 2 64 32 16 4 2 10

U

U

U

= − + + = − + = −

= − + + + = − + + + = −

= − + + + + = − + + + + = −

W przypadku, gdy powielamy bit 0 jest to oczywiste (w rozwinięciu w ogóle nic się nie zmienia).

Podsumujmy podstawowe własności kodu uzupełnień do dwóch.

• Najstarszy (pierwszy od lewej) bit informuje o znaku liczby. Bit 0 mają liczby nieujemne, bit 1 mają liczby ujemne. Np. 3 1 0

2(1011) 2 2 2 5.U = − + + = −

• Zmiana znaku liczby zakodowanej w U2 dokonuje się przez negację poszczególnych bitów kodu i dodanie bitu 1 do najmłodszej (pierwszej od prawej) pozycji. Np. 2(1011) 5.U = − Zatem

2 02 22(1011) 1 (0100) 1 (0101) 2 2 5.U UU + = + = = + =

• Powielanie najstarszego (pierwszego od lewej) bitu nie zmienia wartości zakodowanej liczby. • Zakres liczb w kodzie U2 o długości n wynosi 1 1[ 2 , 2 1].n n− −− − Np. dla 8n = daje to zakres

[ 128, 127],− a dla 16n = zakres [ 32768, 32767].−

Reprezentacje liczb rzeczywistych Matematyczny zbiór liczb rzeczywistych jest nieskończony,2 więc nie można reprezentować

wszystkich liczb rzeczywistych przy pomocy skończonej liczby kombinacji, dostępnej dla kodowania w komputerach. Podobnie zresztą było dla liczb całkowitych , ale w tamtym przypadku można przynajmniej kodować w skończony sposób „odcinki” zawarte w . Natomiast w przypadku liczb rzeczywistych, nawet nie możemy zakodować wszystkich liczb rzeczywistych z przedziału np. [0, 1].

Tak naprawdę w komputerach kodujemy tylko liczby wymierne i to nie wszystkie, bo znów np. na odcinku [0, 1] jest nadal nieskończenie wiele liczb wymiernych (przykładowo

{1/ 2, 1/ 3, 1/ 4, } [0, 1]⊂ ∩ ). Tak więc znane z języków programowania typy rzeczywiste (np. w

Pascalu Real, w języku C/C++ typ float czy double) są to w gruncie rzeczy skończone podzbiory zbioru liczb wymiernych . Przypomnijmy, że liczby wymierne posiadają skończone rozwinięcie dziesiętne lub nieskończone, ale okresowe. Jeżeli więc chcemy prowadzić obliczenie na komputerze, które w teorii są obliczeniami na liczbach rzeczywistych, to aby te obliczenia komputerowe w sensowny sposób aproksymowały naszą abstrakcję, powinniśmy typy rzeczywiste tak konstruować, aby pokrywały one wystarczająco „gęsto” odpowiedni odcinek osi liczbowej. Temu właśnie służą reprezentacje zmiennoprzecinkowe (inaczej: zmiennopozycyjne, ang. floating point numbers).

Zanim omówimy nieco dokładniej pewien standard kodowania liczb rzeczywistych, zaczniemy od kilu przykładów wyjaśniających podstawowe idee. Zasadniczy pomysł przy reprezentacji liczb „z częścią ułamkową” opiera się na następującym podejściu. Jeżeli x∈ , to możemy tę liczbę zapisać następująco:

10 ,cx m= ± ⋅ (0.3) przy czym zakładamy, że:

1 1.10

m≤ < (0.4)

2 Okazuje się, że pojęcie nieskończoności w matematyce ma wiele obliczy. Na przykład zbiór liczb naturalnych oraz zbiór liczb rzeczywistych oba są nieskończone, ale są to różne rodzaje nieskończoności. W szczególności „liczba” elementów zbioru jest „większa” od liczby elementów zbioru . Mówiąc opisowo, elementów zbioru jest więcej niż w elementów zbioru . Oczywiście każdy niepusty odcinek J ⊂ jest również nieskończony i można pokazać, że typ jego nieskończoności jest taki sam jak całego zbioru . Czyli – aby znów odwołać się do języka opisowego – liczba elementów odcinka ( , )a b jest taka sama, jak liczba elementów zbioru . Wydaje się to dziwne, gdyż odcinek jest istotnym podzbiorem zbioru , jednak jest z nim „równoliczny”. Można tym pojęciom nadać precyzyjny sens i rozwinąć tzw. teorię mocy.

Liczbę m nazywamy mantysą, a liczbę c cechą liczby x (przy podstawie dziesiętnej). Okazuje się, że każdą liczbę można tak zapisać, i to w sposób jednoznaczny! Jeżeli jest spełniony warunek (0.4), to mówimy, że postać (0.3) jest znormalizowana.

Przykład

Zapiszmy w postaci znormalizowanej liczby: 12,23; 243,10; 0,00221, 44. Mamy oczywiście

2

3

2

2

12,23 0,1223 10243,10 0,2431 10

0,00221 0,221 1044 0,44 10 .

= + ⋅

− = − ⋅

= ⋅

= ⋅

Podobnie jak dla liczb całkowitych, także w przypadku liczb rzeczywistych przechowywanych w komputerze, lepiej jest używać podstawy dwa zamiast dziesięć. W tym przypadku odpowiednimi wagami przypisanymi do poszczególnych pozycji cyfr (0 i 1) są teraz potęgi dwójki. Na przykład

2 1 0 1 2 3 4 1 1 1 13101,1101 1 2 0 2 1 2 1 2 1 2 0 2 1 2 5 5 5,8125.2 4 16 16

− − − −= ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ + ⋅ = + + + = =

Znormalizowana postać przy podstawie 2 (odpowiednik wzorów (0.3), (0.4)) zdefiniowana jest następująco

12 , 1.2

cx m m= ± ⋅ ≤ <

Podstawą reprezentacji zmiennopozycyjnej jest wyrażenie (0.3) (wraz z warunkiem (0.4)) oraz reguły dotyczące kodowania znaku, cechy i mantysy. Na przykład możemy się umówić, że przy 8-bitowej pamięci przeznaczymy 1 bit na znak, 3 bity na cechę, 4 bity na mantysę przy czym pierwszy bit od lewej strony określa znak liczby, a znak cechy uzyskujemy przez kodowanie jej w kodzie U2.3 Przykładowo wartość reprezentowana przez następującą sekwencję bitów, przy założeniu, że używamy wspomnianej konwencji

0 1 1 0 1 0 1 0

Mamy więc (pierwszy bit to znak) tak: (1010), (110),m c= = tak więc:

1 2 3 4

3 1 1 0

1 1 1 1 51 0 1 02 2 2 2 8

2 1 2 1 2 4 2 2.

m

c −

= ⋅ + ⋅ + ⋅ + ⋅ = +

= − + ⋅ + ⋅ = − + = −

Daje to liczbę

3 W realnych systemach kodowania (np. w opisanym dalej standardzie IEEE 754) nie koduje się znaku cechy dodatkowym bitem znaku. Znak cechy kodowany jest przez specjalną konwencję, polegającą na odejmowaniu tzw. obciążenia (ang. bias). Szczegóły są dalej.

znak liczby

cecha mantysa

25 132 0,203125.8 32

x −= + ⋅ = =

Przykład

Jaka jest największa i najmniejsza liczba dodatnia, które można reprezentować w podanym powyżej, przykładowym kodowaniu zmiennopozycyjnym 8-bitowym.

Największa: 2 1 1 2 3 4 3 45(00111111) (2 2 )(2 2 2 2 ) (8 4 2 1) 5,625.8 8zmp

− − − −= + + + + + = + + + = =

Najmniejsza dodatnia: 3 1 4 6(01000001) (2 0 0)(0 0 0 2 ) 2 0,015625.zmp− − − −= + + + + + + = =

Łatwo jest podać przykład liczby, której nie można reprezentować. Wyliczyć tę liczbę.

Standard zmiennoprzecinkowy IEEE 754 Norma IEEE 754 jest standardem opisującym precyzyjnie sposób kodowania i wykonywania

operacji na pewnym podzbiorze liczb wymiernych. Dzięki niemu możemy wykonywać obliczenia na liczbach, które posiadają część ułamkową.

Reprezentacja liczb w standardzie IEEE 754 W standardzie IEEE 754 określono trzy formaty stałoprzecinkowe dwójkowe (binarne), jeden

stałoprzecinkowy format dziesiętny BCD, oraz cztery formaty zmiennoprzecinkowe. We wszystkich formatach zmiennoprzecinkowych wykładnik jest reprezentowany w kodzie z obciążeniem (ang. bias B). Jeżeli liczba bitów wykładnika (cechy) wynosi e, to obciążenie (zwane też przemieszczeniem) wynosi

12 1.eB −= −

Na przykład dla 8e = mamy 8 1 72 1 2 1 127.B −= − = − =

Dalej opiszemy dokładniej format zmiennopozycyjny zwykły o pojedynczej precyzji. Ciąg bitów dzielimy na trzy części: znak, cecha i mantysa jak poniżej

znak cecha mantysa

W formacie tym:

całkowita liczba wszystkich bitów wynosi 32,

liczba bitów cechy wynosi 8,e =

liczba bitów mantysy wynosi 23.f =

Wartość liczby znormalizowanej jest obliczana w regularnych przypadkach wg wzoru

( 1) 2 1, .z cecha Bv mantysa−= − ⋅ ⋅ (0.5)

Użyte wyżej określenie „regularne przypadku” sugeruje, że nie zawsze wartość przypisywane do kodu w standardzie IEEE 754 jest obliczana wzorem (0.5). W niektórych przypadkach przypisywana wartość jest określona bezpośredni. Na przykład ciągom (1000…00) lub (0000…00) przypisujemy

wartość zero, „dodatnie”, +0 oraz „ujemne”, -0 (oczywiście matematycznie są to te same wartości, tylko inaczej reprezentowane).

Ponieważ w formacie pojedynczej precyzji obciążenie 127,B = więc możemy ten wzór zapisać jako

127( 1) 2 1, .z cechav mantysa−= − ⋅ ⋅ (0.6)

Zauważmy, że kodowanie znaku liczby jest proste: 0=„+”, 1=„-”. Jeżeli chodzi o cechę, to znak jest w tym przypadku kodowany niejawnie, poprzez umowę, że od wartości cechy odejmujemy obciążenie, które dla formatu pojedynczej precyzji wynosi 127.B = Tak więc zakres wartości cechy wynosi

{ 127, 126, ,128,129}.− − Przykład

Dany jest kod liczby w formacie pojedynczej precyzji

0 0 1 1 1 1 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Oblicz wartość liczbową tego kodu.

Rozwiązanie 2 3 4 5 601111100 2 2 2 2 2 124cecha = = + + + + =

2 301100...0 2 2mantysa − −= = +

2 3 111, 1 2 28

mantysa − −= + + =

Zatem 0 124 127 311 11 11( 1) 2 2 0,171875.8 8 64

v − −= − ⋅ ⋅ = ⋅ = =

Odp. Wartością liczbową podanego kodu jest liczba, która w zapisie dziesiętnym wynosi 0,171875.

Pewna komplikacja z poprawnym odczytywaniem wartości przypisanych do kodów jest to, że norma definiuje dwie kategorie liczb – liczby znormalizowane i liczby zdenormalizowane – i dwa sposoby obliczania ich wartości.

Liczby znormalizowane mają cechę niezerową, a dokładniej od 1 do 2e-2, czyli dla standardu pojedynczej precyzji od 1 do 28-2=126. Wartość liczby znormalizowanej obliczana jest wg wzoru (0.6). Natomiast liczby zdenormalizowane mają cechę równą zero (ale mantysę niezerową) i wartość obliczana jest wg poniższego wzoru

126( 1) 2 0, .zv m−= − ⋅ ⋅ (0.7)

Na przykład poniższa liczba jest zdenormalizowana

0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

której wartość wynosi

0 126 126 38 392 3

1 1( 1) 2 0,01100 0 2 1,75510 0,375 4,40810 .2 2

v − − − − = − ⋅ ⋅ = + ≈ ⋅ ≈

(0.8)

Jaka jest najmniejsza dodatnia liczba, którą możemy reprezentować w opisywanym standardzie pojedynczej precyzji? Inaczej mówiąc, jak blisko możemy podejść do zera (od prawej strony)? Jeżeli spojrzymy na wyrażenie (0.6) i (0.7), to łatwo się przekonać, że powinna to być liczba zdenormalizowana. (Mogłoby się wydawać, że powinniśmy wybrać takie bity, aby 0mantysa = oraz

0.cecha = Ale taki kod składający się z samych zer (ewentualnie z jedynką na początku), nie interpretujemy ani wg wyrażenia (0.6) ani wg (0.7), tylko przypisujemy mu wartość 0.) Dlatego musimy wziąć (00000000)c = oraz (000 01),m = czyli

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

któremu odpowiada wartość (wg (0.7), bo jest to liczba zdenormalizowana)

( ) 149126 23 149 log 2 0,30103 149,

44,8534 45

2 (0 2 ) 2 10 10

10 1,401 10 .min denv

−− − − − ⋅

− −

= ⋅ + = = ≈ ≈

≈ ≈ ⋅

Z drugiej strony możemy zapytać jaka jest największa liczba zdefiniowana w kodzie pojedynczej precyzji? Ponownie patrząc na wzór (0.6) spodziewamy się, że należy brać same jedynki dla cechy i mantysy. Ale ponieważ w normie IEEE 754 zarezerwowano specjalne znaczenie dla kodu, którym cecha składa się z samych jedynek, to tak naprawdę największa liczba ma (11111110).c = Daje to kod

0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

któremu odpowiada wartość

( )

240 254 127 1 2 23 127 128

128log 2 0,30103 128 38,532 38

1 (1/ 2)( 1) 2 (1 2 2 2 ) 2 21 (1/ 2)

10 10 10 3,403 10 .

v−

− − − −

−= − ⋅ ⋅ + + + + = ≈ =

= ≈ ≈ ≈ ⋅

Jaka jest odległość pomiędzy największą liczbą a drugą w kolejności? Wiemy już z powyższego

rachunku, że

24

127 128 1041 (1/ 2)2 2 2 .1 (1/ 2)maxv

−−= = −

Najbliższa od lewej strony ma oczywiście kod

0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0

któremu odpowiada wartość

23

0 254 127 1 2 22 127 128 1051 (1/ 2)( 1) 2 (1 2 2 2 ) 2 2 2 .1 (1/ 2)

v−

− − − − −= − ⋅ ⋅ + + + + = = −

Zatem 128 104 128 105 105 104 104 312 2 (2 2 ) 2 2 2 2,03 10 .maxv v− = − − − = − = ≈ ⋅

Jaka jest odległość pomiędzy dwoma liczbami pojedynczej precyzji najbliższymi zeru od prawej

strony? Jeżeli weźmiemy pod uwagę liczby zdenormalizowane, to nieco wyżej wyliczyliśmy, że 126 23 45

,0 2 2 1,4 10 .min denv − − −< = ⋅ ≈ ⋅ Kolejne liczba (zdenormalizowana) będzie miała kod

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

któremu odpowiada wartość (wg wzoru (0.7))

0 126 22 128 39( 1) 2 (0 2 ) 2 2,938 10 .v − − − −= − ⋅ ⋅ + = ≈ ⋅

Zatem różnica pomiędzy dwoma dodatnimi najbliższymi zeru liczbami (zdenormalizowanymi) wynosi

128 129 129 39, 2 2 2 1,469 10 .min denv v − − − −− = − = = ⋅

Dla dwóch najmniejszych dodatnich liczb znormalizowanych różnica ta jest rzędu 3810 .− Jak widać z powyższych rozważań liczby reprezentowalne zmiennopozycyjne nie są rozmieszczone równomiernie na osi liczbowej. Dla pojedynczej precyzji: w pobliżu zera są one rozmieszczone z gęstością rzędu 3810 ,− ale w pobliżu końca zakresu z gęstością rzędu 3010 .

Z przykładów tych widać jasno, że używając standardowych typów danych (float, double itd.) nie możemy na ogół wykonywać obliczeń z dowolna dokładnością. Należy zatem pamiętać, że algorytmy działające na abstrakcyjnych liczbach z (czy ) będą na ogół dawać inne wyniki niż, gdy działają na dziedzinie licz reprezentowalnych. Przykładowo ten sam algorytm

Dziedzina: x∈ Dziedzina: x∈float

x=1.0; while (x>0) { cout << x << ”\n”; x = x/2.0; }

x=1.0; while (x>0) { cout << x << ”\n”; x = x/2.0; }

da odmienne rezultaty, gdy będzie „pracował” w dziedzinie oraz w dziedzinie liczb

reprezentowalnych float. W tym pierwszym przypadku pętla jest nieskończona, ale w tym drugim oczywiście nie: w pewnym momencie „zejdziemy” poniżej najmniejszej wartości dodatniej (pamiętamy, dla formatu pojedynczej precyzji najmniejsza liczba dodatnia (zdenormalizowana) to ok.

451, 401 10 ,−⋅ a zatem x przyjmie wartość zero w skończonej liczbie obiegów powyższej pętli while.)

Poniżej jest tabelka opisująca podstawowe obiekty standardu IEEE 754 (liczby zwykłe (znormalizowane), liczby nie znormalizowane (zdenormalizowane), zera maszynowe, nieskończoności maszynowe oraz kody, które nie odpowiadają żadnym liczbom, tzw. NaNs (od ang. Not a Number – nie liczba).

Typ Cecha Mantysa

Zera (±0) 0 0

Liczba Znormalizowana 1,...,254 Dowolna

Liczba Zdenormalizowana 0 ≠0

±∞ 255 0

NaNs (Not Numbers) 255 ≠0

Własności arytmetyki zmiennopozycyjnej Dotychczas opisywaliśmy tylko sposób reprezentowania liczb (całkowitych lub wymiernych) w

komputerze. Oczywiście liczby te są przydatne tylko pod warunkiem, ze mamy zdefiniowane na nich operacje arytmetyczne (+, ·, -, /). Jak już wiemy zbiór liczb reprezentowalnych F ⊂ jest

skończony i nierównomiernie rozmieszczony na osi liczbowej. Wszelkie obliczenie, które wykonujemy w programach komputerowych, gdy używamy standardowych typów liczbowych są tak naprawdę wykonywane w zbiorze F a nie w zbirze czy . Tak więc już na etapie wprowadzania danych

mamy prawie zawsze do czynienia z przybliżeniami. Jest to źródło tzw. błędów obcięcia (czasami zwanych błędami reprezentacji czy błędami zaokrąglenia). Jeżeli na przykład w programie mamy fragment

... float x; cout << ”Podaj wartość x: ”; cin >> x;

...

i użytkownik wpisze wartość 0.3, to oczywiście nastąpi przypisanie x = 0.3, ale w pamięci zarezerwowanej dla zmiennej x pojawi się reprezentacja zmiennopozycyjna, której wartość nie będzie dokładnie równa matematycznej liczbie 0.3, gdyż ta liczba nie jest reprezentowalna w formacie IEEE 754 – nastąpi właśnie błąd obcięcia. Co więcej, jeżeli teraz będziemy wykonywali operacje arytmetyczne na takich liczbach, np. ,x y+ to na ogół wartość nie będzie równa

matematycznej sumie, nawet gdy obie liczby były reprezentowane dokładnie. Inaczej, jeżeli , ,x y F∈ to na ogół mamy ,x y F+ ∉ co oznacza, że komputer nie obliczy matematycznej sumy

x y+ tylko pewne przybliżenie. Dlatego w rozważaniach nad arytmetyką zmiennopozycyjną często

używa się innych symboli, np. dla sumy x y⊕ czy .x y Wygodnie też jest wprowadzić funkcję,

która opisuje zaokrąglenie, : ,fl F→ określoną tak: dla ,x∈ to ( )fl x oznacza najbliższą jej

liczbę ze zbioru .F Oczywiście dla x F∈ mamy ( ) .fl x x= Ściśle rzecz biorąc określenie ( )fl x F∈

podane przed chwila dotyczy przypadku, gdy liczba nie jest zbyt duża i nie jest zbyt bliska zeru: dokładniej obowiązuje ona, gdy

[ , ] [ , ].max min min maxx x x x x∈ − − ∪

W pozostałych przypadkach, tzn. dla lubmax maxx x x x< − > lub ( , ) \{0}min minx x x∈ − mówimy o

nadmiarze (ang. overflow) lub niedomiarze (ang. underflow). Sytuacje te na ogół powodują wygenerowanie błędu (szczegóły zależy od implementacji). Podstawowa własność arytmetyki zmiennopozycyjnej jest następująca:

jeżeli x∈ jest taka, że | | ,min maxx x x≤ ≤ to

( ) (1 ), przy czym | | 2 ,tfl x x δ δ −= + ≤ (0.9) gdzie t oznacza liczbę bitów mantysy.

Wielkość 2 tu −= jest nazywana precyzją arytmetyki (ang. roundoff unit) lub precyzja maszynową (ang. machine precision). Na przykład dla standardu pojedynczej precyzji (normy IEEE

754) mamy 23,t = zatem 23 72 1,19 10 ,u − −= ≈ ⋅ a dla podwójnej precyzji 52,t = zatem 52 162 2,22 10 .u − −= ≈ ⋅

Jak już wcześniej stwierdzono, konieczne jest zdefiniowanie na zbiorze liczb maszynowych F arytmetyki, która będzie analogiczna – na ile jest to możliwe – do arytmetyki w zbiorze . Na przykład dla dodawania :+ × → odpowiednik zmiennopozycyjny oznaczymy przez ,⊕ tzn.

: .F⊕ × → Jego definicja jest następująca

: ( ( ) ( )).x y fl fl x fl y⊕ = + (0.10)

Ze względu na właściwości liczb maszynowych – (0.9) – oczekujemy, że operator maszynowy ⊕ będzie miał podobną własność: , ,x y F δ∀ ∈ ∃ ∈ taka, że

( )(1 ) przy czym | | 2 .tx y x y δ δ −⊕ = + + ≤ (0.11)

Własność ta (również dla pozostałych operatorów -, ·, /) jest spełniona pod warunkiem, że procedury realizujące definicję (0.10) są odpowiednio zdefiniowane (szczegóły pomijamy, gdyż wykracza to poza zakres tego skryptu).

Na koniec zauważmy, że wiele własności arytmetyki liczb rzeczywistych nie zachodzi w arytmetyce zmiennopozycyjnej. Na przykład prawo łączności nie jest spełnione, gdyż na ogół mamy

( ) ( ) .x y z x y z⊕ ⊕ ≠ ⊕ ⊕

Z drugiej strony pewne prawa są zachowane, np. przemienność dodawania i mnożenia – co jest prostą konsekwencją definicji (0.10) oraz przemienności działań w . Mamy bowiem

( ( ) ( )) ( ( ) ( )) .x y fl fl x fl y fl fl y fl x y x⊕ = + = + = ⊕

Zatem

,.

x y y xx y y x⊕ = ⊕

=