Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

obrazek

Autor artykułu: mgr Jerzy Wałaszek

©2019 mgr Jerzy Wałaszek
I LO w Tarnowie

obrazek

Kody binarne

Liczby zmiennoprzecinkowe

SPIS TREŚCI
Podrozdziały

Konwersje

Do obliczeń naukowych potrzebne są liczby rzeczywiste, a opisane wcześniej kody pozwalają zapisywać tylko liczby całkowite.  Liczby ułamkowe w kodzie dwójkowym można wprowadzać przez rozszerzenie zapisu w prawą stronę. Umówmy się, że pozycje o numerach 0, 1, 2, ... oznaczają wagi całkowite o wartości 2numer pozycji. Tak właśnie są zdefiniowane kody NBC i U2 (w U2 najstarszy bit ma zawsze wagę ujemną). Numerowanie pozycji nie musimy zakończyć na pozycji 0. Możemy pójść dalej w prawo, nadając pozycjom numery ujemne.

Na przykład umówmy się, że wagi pozycji są dalej potęgami liczby 2 o wykładniku równym numerowi pozycji:

Waga ... 22 21 20 2-1 2-2 2-3 ...
Bit ... b2 b1 b0 b-1 b-2 b-3 ...
Pozycja ... 2 1 0 -1 -2 -3 ...

Jeśli wyliczymy wagi pozycji, otrzymamy:

Waga ... 4 2 1 1/2 1/4 1/8 ...
Bit ... b2 b1 b0 b-1 b-2 b-3 ...
Pozycja ... 2 1 0 -1 -2 -3 ...

Zwróć uwagę, że wagi pozycji ujemnych są ułamkowe. Wartość liczby obliczamy tak samo jak przy poznanych wcześniej kodach NBC i U2: sumujemy wagi pozycji, na których występują bity o stanie 1.

Zobaczmy jak to działa na prostym przykładzie. Załóżmy, że mamy kod 8-bitowy. Ustalamy położenie przecinka w środku słowa kodowego. Jest to położenie umowne, ponieważ w kodzie możemy umieszczać tylko bity 0 lub 1. Mamy słowo kodowe 1110101. Jaką ma ono wartość w tym kodzie? Skoro przecinek jest w środku słowa, to mamy 4 bity całkowite oraz 4 bity ułamkowe:

1110,0101

Bity całkowite mają wartość:

11102 = 8 + 4 + 2 = 14

Bity ułamkowe mają wartość (sumujemy wagi pozycji z bitem 1):

,01012 = 1/4 + 1/16 = 4/16 + 1/16 = 5/16

Łączymy wyniki i otrzymujemy wartość liczby przedstawionej tym kodem:

1110,01012 = 14 5/16 = 14,0315

Jak widzisz, liczba nie jest całkowita.

Jak policzyć zakres wartości liczb w takim kodzie? Bardzo prosto. Część całkowita jest zwykłą liczbą NBC (część ułamkowa też, lecz z uwagi na wagi ułamkowe, potraktujmy ją osobno). Zakres części całkowitej: od 0 (00002) do 15 (11112). Zakres części ułamkowej: od 0 (,00002) do 15/16 (,11112). Sumujemy odpowiednio oba zakresy i otrzymujemy, że kod ten może przedstawiać liczby od 0 do 1515/16 z rozdzielczością 1/16.

Postaraj się udowodnić, że w kodzie n-bitowym z m bitami ułamkowymi (n > m) zakres liczb wynosi od 0 do 2n - m - 1 + (2m - 1)/ 2m.

Tego typu kody noszą nazwę kodów stałoprzecinkowych (ang. fixed-point codes). W obliczeniach wykorzystuje się inny rodzaj kodów, zwanych kodami zmiennoprzecinkowymi (floating-point codes). Z zapisem zmiennoprzecinkowych mogłeś się już spotkać na różnych przedmiotach ścisłych, gdzie występowała potrzeba zapisu bardzo dużych lub bardzo małych liczb:

3,14 · 1050 = 3140000000000000000000000000000000000000000000000000
6,53 · 10-30 = 0,00000000000000000000000000000653

Zaletą takiego zapisu jest tutaj większa czytelność. Jednak przyjrzyjmy się bliżej tak zapisanej liczbie. Składa się ona z 3 osobnych części:

m · pc
m – mantysa
p – podstawa
c – cecha

Jeśli umówimy się, że podstawa p jest stała, to nie musimy ją zapisywać. Zostanie nam para liczb (c,m). Cecha c jest liczbą całkowitą. Mantysa m jest liczbą stałoprzecinkową. Możemy teraz skonstruować binarny kod zmiennoprzecinkowy o długości 8 bitów:

p = 2, podstawa jest zawsze stała i nie zapamiętujemy jej w kodzie FP.
c – 4 bitowa liczba całkowita U2
m – 4 bitowa liczba stałoprzecinkowa U2

Podział bitów w naszym słowie kodowym FP będzie następujący:

cecha mantysa
-8 4 2 1 -1 1/2 1/4 1/8
c3 c2 c1 c0 m0 m-1 m-2 m-3

Wartość liczby w tym kodzie jest równa m · 2c. Policzmy wartości kilku kodów.

00000010FP = ?

Rozdzielamy bity kodu FP na bity cechy i bity mantysy:

c = 0000U2 = 0
m = 0,010U2 = 1/4
p = 2

0000010FP = m · pc = 1/4 · 20 = 1/4 · 1 = 1/4 = 0,25

01101110FP = ?
c = 0110U2 = 6
m = 1,110U2 = -1 + 1/2 + 1/4 = -1/4
p = 2

01101110FP = m · pc = -1/4 · 26 = -1/4 · 64 = -16

10001111FP = ?
c = 1000U2 = -8
m = 1,111 = -1 + 1/2 + 1/4 + 1/8 = -1/8
p = 2

10001111FP = m · pc = -1/8 · 2-8 = -1/8 · 1/256 = -1/2048 = -0,00048828125

01110111FP = ?
c = 0111U2 = 4 + 2 + 1 = 7
m = 0,111 = 1/2 + 1/4 + 1/8 = 4/8 + 2/8 + 1/8 = 7/8
p = 2

01110111FP = 7/8 · 27 = 7/8 · 128 = 112

Jak widzisz, kod FP potrafi zapisywać liczby całkowite i ułamkowe dodatnie i ujemne. Obliczenie wartości kodu nie jest takie proste jak w kodach NBC i U2, które są częścią składową kodu FP.

Przejdźmy do własności liczb zmiennoprzecinkowych. Tutaj bardzo przyda się nasz przykładowy kod FP z uwagi na jego prostotę. Na początek policzmy zakres liczb FP w naszym kodzie.

Liczbę najmniejszą dostaniemy dla najmniejszej (najbardziej ujemnej) mantysy i największej cechy:

Największa cecha: c = 0111U2 = 7
Najmniejsza mantysa: m = 1,000U2 = -1

Najmniejsza wartość: 01111000FP = -1 · 27 = -128

Największą liczbę otrzymamy dla największej cechy i największej mantysy:

Największa cecha: c = 0111U2 = 7
Największa mantysa: m = 0,111U2 = 7/8

Największa wartość: 01110111FP = 7/8 · 27 = 7/8 · 128 = 112

Podsumowując, zakres wynosi od -128 do 112.

Zwróć uwagę, że wzór na granice zakresu jest następujący:

min(m) · 2max(c) ... max(m) · 2max(c)

Ponieważ mantysa jest liczbą mniejszą od 1, to zakres zależy głównie od cechy. Im więcej bitów przeznaczymy na cechę, tym bardziej wzrośnie zakres liczb możliwych do przedstawienia w danym kodzie. Zapamiętaj to. Na co wpływają bity mantysy? Na precyzję liczby FP. Im więcej bitów ma mantysa, tym dokładniej można przedstawić określone wartości. Pokażemy to na przykładach

Załóżmy, że chcemy w naszym kodzie przedstawić liczbę 12. Jak to zrobić? Otóż musimy otrzymać wartość mantysy mniejszą od 1 i wartość cechy. Zapisujemy:

12 = 12 · 20 – mantysa za duża, dzielimy przez 2, a cechę zwiększamy o 1.
12 =   6 · 21 – mantysa wciąż jest za duża, dzielimy ją przez 2 i zwiększamy cechę o 1.
12 =   3 · 22 – kontynuujemy podział.
12 = 3/2 · 23 – kontynuujemy.
12 = 3/4 · 24 – mamy mantysę mniejszą od 1. Zatrzymujemy się.

Otrzymaliśmy:

c = 4 = 0100U2
m = 3/4 = 1/2 + 1/4 = 0,110U2

Otrzymujemy:

12 = 01000110FP

Liczbę 12 dało się przedstawić bez problemu. Przyjmijmy roboczo, że przez precyzję liczby binarnej będziemy rozumieli liczbę bitów od pierwszego bitu 1 do ostatniego bitu 1. Liczba 12 w kodzie NBC ma zapis 1100NBC, posiada zatem precyzję 2 bitów. Mantysa naszego kodu ma precyzję 3 bitów, zatem liczba 12 mieści się w kodzie.

Weźmy teraz liczbę 9, której kod NBC ma postać 1001. Tym razem precyzja wynosi 4 bity. Czy da się tę liczbę zapisać w naszym kodzie? Operację dzielenia przez 2 możemy zastąpić prostą operacją przesuwania bitów w prawo. Zapiszmy liczbę 9 w kodzie stałoprzecinkowym z 3 bitami ułamkowymi i rozpocznijmy przesuwanie bitów w prawo tak, aby najstarszy bit trafił za przecinek, wtedy otrzymamy mantysę mniejszą od 1:

mantysa cecha
1001,000 0000
100,100 0001
10,010 0010
1,001 0011
0,1001 0100

Zwróć uwagę, że w czwartym przesunięciu najmłodszy bit 1 wyszedł poza bity ułamkowe mantysy. Dlatego otrzymujemy:

c = 0100U2 = 4
m = 0,100U2 = 1/2

01000100FP = 1/2 · 24 = 1/2 · 16 = 8

Z liczby 9 zrobiła nam się liczba 8. Dlaczego? Ponieważ liczba 9 wymaga 4 bitów precyzji, a mantysa udostępnia jedynie 3 bity. Następuje zatem utrata precyzji i wynikowa liczba obarczona jest błędem zaokrąglenia (ang. rounding error). Jest to typowa własność liczb FP. Zwiększając liczbę bitów mantysy, zwiększamy precyzję, a zatem dokładność liczb.

Zapamiętaj:
Liczby zmiennoprzecinkowe są liczbami przybliżonymi. Dokładnie da się przedstawić tylko takie wartości, których precyzja bitowa mieści się w precyzji bitowej mantysy liczby zmiennoprzecinkowej.

Istnieją pewne wartości, których nigdy nie da się przedstawić dokładnie w kodzie FP. Taką typową wartością jest np. 1/5. Dlaczego? Liczba FP jest sumą potęg liczby 2. Liczba 1/5 nie daje się przedstawić jako suma potęg liczby 2.

Dostajemy nieskończony ułamek binarny. Podobną własność w systemie dziesiętnym mają na przykład liczby:

Wykonując obliczenia, należy zwracać na to uwagę, co pokażemy w następnym podrozdziale.

W języku C++ dostępne są trzy typy dla danych zmiennoprzecinkowych:

Typy zmiennoprzecinkowe
Typ Rozmiar Zakres Precyzja
float 32 bity – 4 bajty Zakres ±3,4 · 1038 7...8 cyfr
double 64 bity –  8 bajtów Zakres ±1,8 · 10308 15...16 cyfr
long double 80 bitów – 10 bajtów Zakres ±1,1 · 104932 19...20cyfr

Współczesne mikroprocesory wyposażone są w tzw. koprocesor arytmetyczny. Jest to wyspecjalizowany moduł, który wykonuje szybkie obliczenia na liczbach zmiennoprzecinkowych. Typ long double jest typem danych wykorzystywanych wewnętrznie przez koprocesor. Zapewnia on wysoką dokładność obliczeń. W mikroprocesorach kompatybilnych z Pentium typ long double ma długość 80 bitów, czyli 10 bajtów. Jednak na innych platformach typ ten może posiadać inny rozmiar.

Precyzja odnosi się tutaj do gwarantowanej liczby początkowych cyfr dziesiętnych liczby zmiennoprzecinkowej, które zostaną zapamiętane dokładnie. Przykładowo w typie float (dostępny, lecz obecnie niezalecany z uwagi na niską precyzję) precyzja obejmuje około 7 pierwszych cyfr liczby. Jeśli liczba posiada więcej cyfr, to dokładnie zostanie zapamiętane tylko 7... 8 pierwszych z nich.

Poniższy program demonstruje tę własność dla trzech typów danych:

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    cout << setprecision(0) << fixed;
    cout << "Precyzja liczb zmiennoprzecinkowych" << endl
         << "-----------------------------------" << endl;

    long double x;
    int i;

    x = 1;

    cout << "Cyfry                   float                  double             long double" << endl;

    for(i = 1; i < 21; i++)
    {

        cout << setw( 5) << i
             << setw(24) << (float) x
             << setw(24) << (double) x
             << setw(24) << x << endl;

        x *= 10;
        x += (i + 1) % 10;
    }

    cout << endl;

    return 0;
}
Wynik
Precyzja liczb zmiennoprzecinkowych
-----------------------------------
Cyfry                float                double           long double
    1                    1                     1                     1
    2                   12                    12                    12
    3                  123                   123                   123
    4                 1234                  1234                  1234
    5                12345                 12345                 12345
    6               123456                123456                123456
    7              1234567               1234567               1234567
    8             12345678              12345678              12345678
    9            123456792             123456789             123456789
   10           1234567936            1234567890            1234567890
   11          12345678848           12345678901           12345678901
   12         123456790528          123456789012          123456789012
   13        1234567954432         1234567890123         1234567890123
   14       12345679020032        12345678901234        12345678901234
   15      123456788103168       123456789012345       123456789012345
   16     1234567948140544      1234567890123456      1234567890123456
   17    12345678407663616     12345678901234568     12345678901234567
   18   123456790519087104    123456789012345680    123456789012345678
   19  1234567939550609408   1234567890123456768   1234567890123456789
   20 12345679395506094080  12345678901234567168  12345678901234567890

Typ long double jest najbardziej "precyzyjny", jednakże nie jest standardowy (na innych platformach może być taki sam jak typ double). Dlatego do zwykłych obliczeń stosujemy typ double, zwany typem o podwójnej precyzji (podwójnej w stosunku do typu float).

Typ float był intensywnie używany w przeszłości, gdy komputery nie miały dużej pamięci i były wolne (starsze mikroprocesory nie posiadały koprocesora arytmetycznego, który był osobnym układem scalonym, dosyć drogim i nie zawsze instalowanym na płycie głównej komputera – wszystkie operacje zmiennoprzecinkowe realizował programowo mikroprocesor, co było dosyć czasochłonne) . Dzisiaj lepiej go już nie stosować.

Na początek:  podrozdziału   strony 

Operacje arytmetyczne

Dodawanie/odejmowanie liczb zmiennoprzecinkowych

Nie będziemy się zagłębiać w szczegóły techniczne wykonywania operacji na liczbach zmiennoprzecinkowych. Opiszemy jedynie ich podstawowe własności, które powinieneś znać, aby uniknąć pułapek w trakcie programowania.

Liczbę zmiennoprzecinkową możemy ogólnie zapisać w postaci wyrażenia m · 2c, gdzie m jest mantysą, c jest cechą.  Załóżmy, że mamy dwie liczby zmiennoprzecinkowe a i b:

Dodanie tych dwóch liczb do siebie polega na znalezieniu mantysy i cechy wyniku dodawania:

W tym celu wyrównujemy cechy obu liczb:

Otrzymaną w ten sposób mantysę należy znormalizować, tzn. doprowadzić do wartości poprawnej dla danego kodu, np. w naszym przykładowym kodzie 8-bitowym mantysa jest liczbą mniejszą od 1 i większą lub równą -1. Mnożenie przez potęgi liczby 2 jest dokonywane przez przesuwanie bitów. Również normalizacja jest wykonywana za pomocą przesunięć. Gdy bity mantysy są przesuwane w lewo, cecha maleje o 1; gdy są przesuwane w prawo, cecha rośnie o 1. Zobaczmy jak wygląda to na przykładzie.

Mamy dodać dwie liczby 8 i 6. Najpierw znajdźmy ich kody FP:

8 = 1/2 · 24

c1 = 4 = 0100U2
m1 = 1/2 = 0,100U2
8 = 01000100FP

6 = (1/2 + 1/4) · 23

c2 = 3 = 0011U2
m2 =1/2 + 1/4 = 0,110U2

6 = 00110110FP

Wyrównujemy wykładniki. W tym celu mantysę m2 przesuwamy bitowo w prawo o 1 pozycję, a wykładnik c2 zwiększamy o 1:

m2' = m2 / 2 = 1/4 + 1/8 = 0,011U2
c2' = c2 + 1 = 3 + 1 = 4 = 0100U2

Teraz obie liczby mają ten sam wykładnik, możemy dodać mantysy i otrzymamy mantysę wyniku:

m1+2 = m1+m2' = 1/2 + 1/4 + 1/8 = 7/8 = 0,111U2

Mantysa mieści się we właściwym przedziale, więc liczby nie musimy normalizować. Otrzymujemy kod wynikowy:

01000111FP = 7/8 · 24 = 7/8 · 16 = 14
8 + 6 = 14

Wynik jest prawidłowy.

Inny przykład. Dodajemy 14 + 14:

14 = 01000111FP (wykorzystujemy gotowy kod z poprzedniego przykładu)

c12 = 0100U2 = 4
m12 = 0,111U2 = 7/8

Ponieważ oba wykładniki są takie same, nie musimy ich wyrównywać. Sumujemy mantysy:

m = m12 + m12 = 7/8 + 7/8 = 14/8 = 1 6/8 = 01,110U2
c = c12 = 4 = 0100U2

Mantysa jest większa od 1, musimy ją znormalizować. Przesuwamy jej bity o 1 pozycję w prawo i zwiększamy wykładnik o 1:

m' = 0,111U2 = 7/8,  c' = c + 1 = 5 = 0101U2

Teraz mantysa jest mniejsza od 1, mamy gotowy kod:

01010111FP = 7/8 · 25 = 7/8 · 32 = 28
14 + 14 = 28

Wynik sumowania może być niepoprawny, jeśli nastąpi zaokrąglenie mantysy. Zsumujmy liczbę 8 i 3:

8 = 1/2 · 24

c1 = 4 = 0100U2
m1 = 1/2 = 0,100U2
8 = 01000100FP

3 = 3/4 · 22

c2 = 2 = 0010U2
m2 = 3/4 = 0,110U2
3 = 00100110FP

Wyrównujemy wykładniki, mantysę m2 przesuwamy o 2 bity w prawo, a wykładnik c2 zwiększamy o 2:

m2 = 0,110U2 = 3/4, c2 = 0010U2 = 2
m2' = 0,0011U2 = 3/16, c2' = 0100U2 = 4

Zwróć uwagę, iż po przesunięciu, najmłodszy bit mantysy m2' wyszedł poza bity mantysy standardowej, dlatego zaznaczyliśmy go tutaj na czerwono.

Wykładniki są wyrównane, dodajemy mantysy obu liczb:

m = m1 + m2' = 1/2 + 3/16 = 11/16 = 0,1011U2

Mantysa wynikowa jest mniejsza od 1, nie musimy normalizować wyniku. Jednak najmłodszy bit mantysy wynikowej musi zostać odrzucony, ponieważ nie mieści się w bitach mantysy kodu FP. Dokonujemy tzw. zaokrąglenia

m = 0,101U2 = 5/8

i otrzymujemy:

01000101FP = 5/8 · 24 = 5/8 x· 16 = 10
8 + 3 = 10

Wynik jest niepoprawny, ponieważ liczba 11 wymaga 4 bitów precyzji (11 = 1011NBC), a mantysa udostępnia jedynie 3 bity precyzji.

Przedstawiona tutaj sytuacja występuje również w działaniach na liczbach zmiennoprzecinkowych w języku C++ (jest to cecha wspólna wszystkich kodów zmiennoprzecinkowych).

Poniższy program zwiększa o 1 najpierw małą liczbę typu float, a później robi to samo z dużą liczbą typu float. Wyciągnij z tego wnioski:

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    cout << setprecision(0) << fixed;
    cout << "Precyzja liczb zmiennoprzecinkowych" << endl
         << "-----------------------------------" << endl;

    float x;

    x = 999;

    cout << "Mała liczba float" << endl
         << "Przed : " << x;
    x++;
    cout << " Po : " << x << endl << endl;

    x = 100000000;

    cout << "Duża liczba float" << endl
         << "Przed : " << x;
    x++;
    cout << " Po : " << x << endl << endl;

    return 0;
}
Wynik
Precyzja liczb zmiennoprzecinkowych
-----------------------------------
Mała liczba float
Przed : 999 Po : 1000

Duża liczba float
Przed : 100000000 Po : 100000000

Jak napisaliśmy w poprzednim rozdziale, niektóre wartości tworzą nieskończone ułamki binarne i nie dają się dokładnie zapisać w żadnym z typów danych zmiennoprzecinkowych. Taką wartością jest na przykład 0,1 (jedna dziesiąta). Przy sumowaniu takich wartości błędy zaokrągleń kumulują się i wynik może odbiegać od wartości dokładnej.

Poniższy program dodaje do zmiennej o początkowej wartości 0 dziesięć razy liczbę 0,1. Następnie odejmuje od tej zmiennej 1. Jeśli wyjdzie wartość 0 (tak wynika z arytmetyki), to wyświetla napis "DOBRZE". Jeśli wynik odejmowania jest różny od zera, to wyświetla napis "ŹLE" i wypisuje wartość zmiennej z 20 cyframi po przecinku. Wyciągnij wnioski sam:

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    cout << setprecision(20) << fixed;
    cout << "Precyzja liczb zmiennoprzecinkowych" << endl
         << "-----------------------------------" << endl;

    double x = 0;

    // Do zmiennej x dodajemy dziesięć razy liczbę 0,1.

    for(int i = 0; i < 10; i++) x += 0.1;

    // W x powinna być wartość 1.
    // Odejmujemy 1 od x. Powinniśmy otrzymać 0:

    x -= 1;

    if(x == 0) cout << "DOBRZE";
    else       cout << "ŹLE " << x;

    cout << endl << endl;

    return 0;
}
Wynik
Precyzja liczb zmiennoprzecinkowych
-----------------------------------
ŹLE -0.00000000000000011102

Błąd jest bardzo mały, lecz komputer porównuje bity i różnica nawet jednego bitu powoduje, iż wyświetli się napis "ŹLE". Wynika z tego, iż liczb zmiennoprzecinkowych nie powinno się porównywać z liczbami dokładnymi, ponieważ z powodu błędów zaokrągleń wynik może być obarczony pewnym błędem. Jak zatem sprawdzić, czy liczba zmiennoprzecinkowa ma pożądaną wartość? Musisz zdecydować, z jaką dokładnością chcesz przeprowadzić to sprawdzenie. Należy zatem oszacować możliwy błąd, Nie jest to zadanie proste i dokładne metody poznasz dopiero na studiach. Tutaj możemy podejść do problemu w sposób prosty, lecz nieprofesjonalny:

Liczbę zmiennoprzecinkową LFP możemy potraktować jako sumę liczby dokładnej Ld oraz błędu zaokrąglenia Le:

LFP = Ld + Le

Wartość Le zależy od dostępnej precyzji oraz od wartości samej liczby dokładnej Ld. Dlatego zwykle posługujemy się pojęciem błędu względnego:

Ponieważ błąd może być dodatni lub ujemny, to stosujemy wartość bezwzględną tego błędu:

Po podstawieniach mamy:

Jeśli dana reprezentacja FP posiada precyzję n cyfr, to znaczy, że pierwsze n cyfr liczby jest dokładne. Począwszy od cyfry (n + 1) dokładność nie jest gwarantowana i mogą pojawiać się błędy. Jeśli nasza liczba Ld = 0,1, a dokładność wynosi 15 cyfr, to w najgorszym przypadku |Le| = 0,000000000000000999... Dla prostoty przyjmijmy |Le| = 0,000000000000001. Czyli błąd względny ma wartość:

Jeśli zsumujemy 10 razy liczbę FP, to

Po podstawieniach otrzymamy:

Aby sprawdzić, czy suma osiągnęła założoną wartość x nie porównujemy jej z tą wartością bezpośrednio, lecz sprawdzamy, czy wartość bezwzględna różnicy sumy i wartości oczekiwanej jest równa lub mniejsza od oszacowanego błędu:

Jeśli tak, to przyjmujemy, że suma ma w przybliżeniu wartość oczekiwaną.

Wynika z tego następujący wniosek:

Porównanie liczby FP x z inną wartością FP y:
  1. Wyznacz przybliżoną wartość błędu ε.
  2. Sprawdź, czy |x - y| ≤ ε.
  3. Jeśli tak, to przyjmij, że x ≈ y.
  4. Jeśli nie, to przyjmij, że x ≠ y.

Błąd względny zależy od precyzji obliczeń. W przybliżeniu możesz przyjąć (przypadek pesymistyczny), że:

Gdzie p oznacza liczbę cyfr dokładnych w danej precyzji.

Zmieniamy nasz program następująco:

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <cmath>
#include <iomanip>

using namespace std;

const double EPS = 0.00000000001; // Wartość spodziewanego błędu

int main()
{
    setlocale(LC_ALL,"");

    cout << setprecision(20) << fixed;
    cout << "Precyzja liczb zmiennoprzecinkowych" << endl
         << "-----------------------------------" << endl;

    double x = 0;

    // Do zmiennej x dodajemy dziesięć razy liczbę 0,1.

    for(int i = 0; i < 10; i++) x += 0.1;

    // W x powinna być wartość 1.
    // Odejmujemy 1 od x. Powinniśmy otrzymać 0:

    x -= 1;

    if(fabs(x) <= EPS) cout << "DOBRZE";
    else               cout << "ŹLE " << x;

    cout << endl << endl;

    return 0;
}
Wynik
Precyzja liczb zmiennoprzecinkowych
-----------------------------------
DOBRZE

Z tej zasady będziemy korzystali zawsze przy porównywaniu liczb zmiennoprzecinkowych. Zapamiętaj ją.


Odejmowanie jest dodawaniem liczby przeciwnej:

a - b = a + (-b)

Jeśli odejmujesz liczby prawie równe, to wynik może być bardzo niedokładny. Dlaczego? Wyjaśnijmy to na przykładzie. Załóżmy, że mamy precyzję 3 cyfr, tzn. 3 pierwsze cyfry liczby są dokładne, reszta już nie. Weźmy teraz dwie liczby 6 cyfrowe:

L1 = 123??? (znakiem ? oznaczyliśmy cyfry poza naszą precyzją)
L2 = 122???

Odejmujemy:

L1 - L2 = 123??? - 122??? = 0,???

Precyzja wyniku spadła do 1 cyfry, ponieważ pozostałe są niepewne, a w niekorzystnych okolicznościach nawet ta pierwsza cyfra nie jest pewna.

Wniosek:

Staraj się unikać odejmowania od siebie liczb prawie równych, ponieważ wynik może być niedokładny.

Mnożenie/dzielenie liczb zmiennoprzecinkowych

Zasada mnożenia liczb zmiennoprzecinkowych jest następująca:

Mamy 2 liczby zmiennoprzecinkowe:

obrazek

Tworzymy ich iloczyn:

Mantysa wynikowa jest iloczynem mantys obu mnożonych liczb. Cecha wynikowa jest sumą cech mnożonych liczb. Po operacji mnożenia mantysa wynikowa musi zostać znormalizowana, czyli sprowadzona do odpowiedniego zakresu za pomocą przesuwów bitowych, jak przy dodawaniu.

Zobaczmy na przykładzie w naszym prostym systemie FP jak wygląda mnożenie dwóch liczb zmiennoprzecinkowych.

L1 = 8 = 1/2 · 24
m1 = 1/2
c1 = 4
L1 = 01000100FP

L2 = 6 = (1/2 + 1/4) · 23
m2 = 1/2 + 1/4
c2 = 3
L2 = 00110110FP

Mnożymy mantysy:

m = m1 · m2 = 1/2 · (1/2 + 1/4) = 1/2 · 3/4 = 3/8 = 0,011U2

Dodajemy wykładniki

c = c1 + c2 = 4 + 3 = 7 = 0111U2

Mantysę normalizujemy przesuwając jej bity o jedna pozycję w lewo (odpowiada to pomnożeniu liczby przez 2) i zmniejszając o 1 cechę (odpowiada to podzieleniu liczby przez 2, w sumie dostajemy tę samą liczbę):

c = 7 = 0111U2    m = 3/8 = 0,011U2
c = 6 = 0110U2    m = 3/4 = 0,110U2

I otrzymujemy kod wynikowy:

01100110FP = 3/4 · 26 = 3/4 · 64 = 48 = 8 · 6

Dla mnożenia dwóch liczb zmiennoprzecinkowych mamy:

Jeśli precyzja liczby jest duża, to kwadrat błędu jest bardzo mały i można go pominąć:

Przy mnożeniu 3 liczb zmiennoprzecinkowych otrzymujemy:

Możemy to kontynuować dla następnych iloczynów i ostatecznie otrzymamy, że błąd mnożenia n liczb zmiennoprzecinkowych jest w przybliżeniu równy:

Dzielenie jest odwrotnością mnożenia:

Obowiązują tutaj te same zasady, co przy mnożeniu.

Zapamiętaj:
  • Liczby zmiennoprzecinkowe są liczbami przybliżonymi.
  • Precyzja określa liczbę cyfr dokładnych w zapisie dziesiętnym.
  • Pewnych wartości nie można przedstawić dokładnie w systemie zmiennoprzecinkowym, np. 1/5, 2/5,1/3 ...
  • Działania na liczbach zmiennoprzecinkowych obarczone są zwykle błędami zaokrągleń.
  • Liczby zmiennoprzecinkowej x uzyskanej z obliczeń nie należy przyrównywać do wartości dokładnej a, zamiast tego należy sprawdzać, czy:
     

    gdzie ε jest wyznaczoną dokładnością obliczeń. Jeśli powyższa nierówność będzie spełniona, to zakładamy, że x jest równe a z dokładnością ε.

Na początek:  podrozdziału   strony 

Liczby pseudolosowe

Liczby pseudolosowe całkowite opisane zostały w poprzednim rozdziale.

Liczbę pseudolosową rzeczywistą możemy otrzymać z wbudowanego generatora pseudolosowego dzieląc odpowiednio jego wynik. Liczby z przedziału obustronnie domkniętego [0,1] daje nam poniższe wyrażenie:

rand() / (double) RAND_MAX

Rzutowanie na typ double jest konieczne, aby kompilator zastosował dzielenie zmiennoprzecinkowe. W przeciwnym razie dzielenie będzie całkowitoliczbowe z wynikiem 0 (bardzo często, rand() < RAND_MAX) lub 1 (bardzo rzadko, rand() = RAND_MAX).

Poniższy program generuje 10 rzeczywistych liczb pseudolosowych:

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    cout << setprecision(10) << fixed;
    cout << "Zmiennoprzecinkowe liczby pseudolosowe w przedziale [0,1]" << endl
         << "---------------------------------------------------------" << endl << endl;

    // Inicjujemy generator liczb pseudolosowych

    srand(time(NULL));

    double x;

    // Generujemy 10 liczb pseudolosowych

    for(int i = 0; i < 10; i++)
    {
        x = rand() / (double) RAND_MAX;
        cout << x << endl;
    }

    cout << endl;

    return 0;
}
Wynik
Zmiennoprzecinkowe liczby pseudolosowe w przedziale [0,1]
---------------------------------------------------------

0.9625843074
0.5021210364
0.1986449782
0.4655598621
0.1885738701
0.1685537278
0.1208227790
0.0693075350
0.8621173742
0.9065218055

 

Granice przedziału generowanych liczb możemy modyfikować:

(0,1] – otwarty lewostronnie, domknięty prawostronnie:
(rand() + 1) / (double) (RAND_MAX + 1)

(0,1) – otwarty obustronnie:
(rand() + 1) / (double) (RAND_MAX + 2)

[0,1) – domknięty lewostronnie, otwarty prawostronnie:
rand() / (double) (RAND_MAX + 1)

Mając przedział 0...1 możemy go dowolnie rozszerzać, np na przedział a...b:

[a,b] – domknięty obustronnie:
a + (b - a) * (rand() / (double) RAND_MAX)

Poniższy program generuje liczby w zakresie od a do b.

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <iomanip>

using namespace std;

int main()
{
    double a,b,x;

    setlocale(LC_ALL,"");

    cout << setprecision(5) << fixed;
    cout << "Zmiennoprzecinkowe liczby pseudolosowe w przedziale [a,b]" << endl
         << "---------------------------------------------------------" << endl << endl
         << "Podaj granice przedziału:" << endl;

    // Odczytujemy granice przedziału liczb pseudolosowych

    cout << "a = ? "; cin >> a;
    cout << "b = ? "; cin >> b;
    cout << endl;

    // Inicjujemy generator liczb pseudolosowych

    srand(time(NULL));

    // Generujemy 10 liczb pseudolosowych i zamieniamy je
    // na liczby zmiennoprzecinkowe

    for(int i = 0; i < 10; i++)
    {
        x = a + (b - a) * (rand() / (double) RAND_MAX);
        cout << setw(12) << x << endl;
    }

    cout << endl;

    return 0;
}
Wynik
Zmiennoprzecinkowe liczby pseudolosowe w przedziale [a,b]
---------------------------------------------------------

Podaj granice przedziału:
a = ? -200
b = ? 200

  -177.23319
  -146.94662
    -9.66216
   159.54466
   -87.48436
    60.65249
  -171.54454
   -11.73742
  -180.21180
  -109.42106

Jeśli musisz generować zmiennoprzecinkowe liczby pseudolosowe w swoim programie, to lepszym rozwiązaniem jest skorzystanie z biblioteki szablonowej <random>. Poniższy program jest wersją poprzedniego. Różnica dotyczy zakresu generowanych liczb. Przy obiekcie rozkładu uniform_real_distribution parametry a,b określają przedział [a,b) – domknięty lewostronnie, otwarty prawostronnie.

Pamiętaj o ustawieniu w opcjach kompilatora obsługi standardu 11 języka C++:

obrazek

Poniższy program generuje liczby w przedziale [a,b).

Przykładowy program w języku C++
// Liczby zmiennoprzecinkowe
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne
//-----------------------------------------

#include <iostream>
#include <random>
#include <ctime>
#include <iomanip>

using namespace std;

int main()
{
    double a,b,x;

    setlocale(LC_ALL,"");

    cout << setprecision(5) << fixed;
    cout << "Zmiennoprzecinkowe liczby pseudolosowe w przedziale [a,b)" << endl
         << "---------------------------------------------------------" << endl << endl
         << "Podaj granice przedziału:" << endl;

    // Odczytujemy granice przedziału liczb pseudolosowych

    cout << "a = ? "; cin >> a;
    cout << "b = ? "; cin >> b;
    cout << endl;

    // Tworzymy obiekt generatora pseudolosowego i inicjujemy go

    mt19937 gen(time(NULL));

    // Tworzymy klasę rozkładu:
    // Rozkład jednorodny liczb rzeczywistych w przedziale <a,b)

    uniform_real_distribution <double> dist(a,b);

    // Generujemy 10 liczb pseudolosowych

    for(int i = 0; i < 10; i++)
    {
        x = dist(gen);
        cout << setw(12) << x << endl;
    }

    cout << endl;

    return 0;
}
Wynik
Zmiennoprzecinkowe liczby pseudolosowe w przedziale [a,b)
---------------------------------------------------------

Podaj granice przedziału:
a = ? -10
b = ? 10

    -1.49019
    -0.70826
     5.20275
     3.89604
     8.31401
     0.76755
    -7.90011
    -2.96140
     5.07277
    -3.50012

Zastanów się, jak generować liczby pseudolosowe w przedziałach [a,b], (a,b], (a,b) i [a,b) przy wykorzystaniu jedynie samej klasy generatora pseudolosowego (skorzystaj z funkcji składowych min() i max() generatora).

Na początek:  podrozdziału   strony 

Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2019 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.