Serwis Edukacyjny w I-LO w Tarnowie ![]() Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy Wałaszek |
©2023 mgr Jerzy Wałaszek |
SPIS TREŚCI |
|
Podrozdziały |
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ć.
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
· 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:
|
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. |
Zasada mnożenia liczb zmiennoprzecinkowych jest następująca:
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 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
|
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++:
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 ).
![]() |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2023 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.