jesteśmy scientiscs

Transkrypt

jesteśmy scientiscs
Wstęp do programowania
Wykład 4 ­ Reprezentacja liczb
Janusz Szwabiński
Plan wykładu:
Zamiast motywacji
Reprezentacja liczb całkowitych
Reprezentacja liczb rzeczywistych
Dokładność w obliczeniach komputerowych
Materiały uzupełniające:
D. Goldberg, "What Every Computer Scientist Should Know About Floating­Point Arithmetic",
Computing Surveys (1991)
Zamiast motywacji
Z lekcji matematyki wiemy, że cos
π
2
= 0. Zobaczmy, jak to wygląda w Pythonie:
In [7]:
import math
In [8]:
math.cos(math.pi/2)
Out[8]:
6.123233995736766e-17
Wynik jest wprawdzie bardzo mały, jednak różni się od zera.
Inny przykład:
In [9]:
x = 2**30
In [10]:
x + 2**(-22)==x
Out[10]:
False
In [11]:
x + 2**(-23)==x
Out[11]:
True
Okazuje się, że w drugim przypadku komputer traktuje obie liczby jako identyczne!
I jeszcze jeden przykład:
In [12]:
y = 1000.2
In [13]:
x = y - 1000.0
In [14]:
print(x)
0.20000000000004547
Wynik różni się nieznacznie od wartości dokładnej 0.2 !
Powyższe przykłady to efekty skończonej dokładności maszyny liczącej, jaką jest komputer. Nie zawsze są
one szkodliwe dla prowadzonych obliczeń, jednak warto wiedzieć, dlaczego powstają.
Liczby całkowite
Liczby całkowite są reprezentowane dokładnie w pamięci komputera, jednak jest ich skończona liczba. W
większości języków programowania jest to związane ze stałą liczbą bitów używanych do ich
przechowywania.
W ogólności, n­bitowa liczba całkowita ma wartości od ­2^(n­1) do 2^(n­1) ­ 1. Na maszynach 32­bitowych
daje to zakres:
In [15]:
n = 32
imin = -2**(n-1)
imax = 2**(n-1)-1
print(imin,"< i <",imax)
-2147483648 < i < 2147483647
Natomiast dla maszyn 64­bitowych otrzymamy:
In [16]:
n = 64
imin = -2**(n-1)
imax = 2**(n-1)-1
print(imin,"< i <",imax)
-9223372036854775808 < i < 9223372036854775807
W Pythonie jest jednak trochę inaczej. W wersji 2.7 rzeczywiście mamy do dyspozycji typ int o powyższych
zakresach, jednak wyjście poza zakres powoduje automatyczne przełączenie się interpretera na typ long
int o teoretycznie nieograniczonej precyzji (liczbie bitów). W praktyce precyzja jest ograniczona pamięcią
komputera. Natomiast w wersji 3.X nie ma już rozróżnienia na int i long int. Dlatego w Pythonie można
liczyć na liczbach dużo większych niż te zdefiniowane powyżej:
In [17]:
2*10**100
Out[17]:
2000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000
Ponieważ jednak Python należy pod tym względem raczej do wyjątków wśród języków programowania,
omówimy teraz dwie najczęściej stosowane reprezentacje liczb całkowitych, z których ograniczeń użytkownik
Pythona może nie zdawać sobie sprawy.
Reprezentacja prosta (bez znaku)
N ­bitowa liczba całkowita z bez znaku jest reprezentowana jako:
z = (aN −1 , aN −2 , … , a0 ) ,
gdzie ai
= {0, 1} (i = 0, … , N − 1) to poszczególne bity.
Jej wartość wyliczymy w następujący sposób:
z = ∑
N −1
i=0
i
ai ∗ 2 .
Natomiast zakres tej reprezentacji to:
N
0 ≤ z ≤ 2
− 1.
In [18]:
n = 64
imax = 2**(n)-1
print("0 < i <",imax)
0 < i < 18446744073709551615
Przykłady liczb w 8­bitowej reprezentacji:
Liczba Reprezentacja
0
00000000
2
00000010
51
00110011
127
01111111
255
11111111
Reprezentacja uzupełnieniowa (ze znakiem)
N ­bitowa liczba całkowita z ze znakiem na pierwszy rzut oka reprezentowana jest tak samo, tzn.:
z = (aN −1 , aN −2 , … , a0 ) , ai = {0, 1},
jednak teraz wyliczamy jej wartość ze wzoru:
N −1
z = −aN −1 ∗ 2
+ ∑
N −2
i=0
i
ai ∗ 2 .
Zakres tej reprezentacji:
N −1
−2
N −1
≤ z ≤ 2
− 1
Jedną z zalet tej reprezentacji jest łatwe generowanie liczb ujemnych:
Ponadto nie trzeba rozróżniać liczb na dodatnie i ujemne przy dodawaniu:
Poniżej znajdują się przykłady liczb w 8­bitowej reprezentacji uzupełnieniowej:
Liczba Reprezentacja
127
01111111
51
00110011
2
00000010
­2
11111110
­51
11001101
­128
10000000
Warto również wspomnieć, że mnożenie przez 2 w tej reprezentacji odpowiada przesunięciu bitów o jedną
pozycję w lewo, a dzielenie przez 2 ­ o jedną pozycję w prawo.
In [19]:
x=4
In [20]:
x << 1
Out[20]:
8
In [21]:
x >> 1
Out[21]:
2
Jedyny problem w obliczeniach komputerowych na liczbach całkowitych to wyjście poza zakres (podkreślam
jeszcze raz ­ nie dotyczy to Pythona!). Rozważmy następujący przykład w napisany w języku C++:
In [22]:
%%writefile silnia.cpp
#include <iostream>
int silnia(int n)
{
if (n<=1)
return 1;
else
return n*silnia(n-1);
}
int main ()
{
int n;
std :: cout << "Podaj liczbę: ";
std :: cin >> n;
std :: cout << n << "!= " << silnia(n) << "\n";
}
Overwriting silnia.cpp
In [23]:
!g++ silnia.cpp
Efekt kilku wywołań tego programu jest następujący:
Podaj liczbę: 12 12!=479001600 Podaj liczbę: 16 16!=2004189184 Podaj liczbę: 17 17!=­288522240
Mimo,że przy obliczaniu silni mnożymy ze sobą liczby dodatnie, ostatni wynik jest ujemny. Jest to związane z
tym, że najczęściej (i C++ nie jest tu wyjątkiem) typy całkowite mają strukturę pierścienia, w której
zwiększenie liczby największej o 1 da w wyniku liczbę najmniejszą. I odwrotnie ­ zmniejszenie liczby
najmniejszej o 1 da liczbę największą.
Jeżeli nie pamiętamy, jakie zakresy mają poszczególne typy danych, najczęściej możliwe jest ich
podględnięcie z poziomu akurat używanego języka. Np. w C++ odpowiednie stałe zdefiniowane są w pliku
nagłówkowym climits:
In [24]:
%%writefile cints.cpp
#include <iostream>
#include <climits>
int main ()
{
std :: cout << SHRT_MAX << "\n" ;
std :: cout << INT_MAX << "\n" ;
std :: cout << LONG_MAX << "\n" ;
}
Overwriting cints.cpp
In [25]:
!g++ cints.cpp -o cints
In [26]:
!./cints
32767
2147483647
9223372036854775807
W Fortranie do wyświetlenia największej liczby w danym typie służy funkcja huge:
In [27]:
%%writefile fints.f90
program integers
implicit none
integer(kind=1) :: a
integer(kind=2) :: b
integer(kind=4) :: c
integer(kind=8) :: d
integer(kind=16) :: e
write(6,*) huge(a)
write(6,*) huge(b)
write(6,*) huge(c)
write(6,*) huge(d)
write(6,*) huge(e)
end program integers
Overwriting fints.f90
In [28]:
!gfortran fints.f90 -o fints
In [29]:
!./fints
127
32767
2147483647
9223372036854775807
170141183460469231731687303715884105727
Typ int Pythona radzi sobie oczywiście dużo lepiej przy wiliczaniu silni:
In [30]:
def silnia(n):
if n<=1:
return 1
else:
return n*silnia(n-1)
In [31]:
silnia(17)
Out[31]:
355687428096000
In [32]:
silnia(100)
Out[32]:
9332621544394415268169923885626670049071596826438162146859296389521
7599993229915608941463976156518286253697920827223758251185210916864
000000000000000000000000
Należy mieć przy tym świadomość, że "dowolną" precyzję otrzymujemy kosztem wydajności obliczeń. Np.
jeżeli wszystkie liczby występujące w obliczeniach i wyniki działań na nich mieszczą się w zakresie
odpowiadającym podstawowemu typowi int w Pythonie 2.7, wówczas obliczenia w Pythonie 2.7 zostaną
wykonane szybciej niż w 3.5.
Liczby zmiennopozycyjne
Reprezentacja zmiennopozycyjna to najczęściej obecnie stosowany sposób zapisu liczb rzeczywistych na
komputerach. W reprezentacji tej liczba przedstawiona jest za pomocą bazy B oraz precyzji (liczby cyfr) p.
Przykład: jeśli B
−1
= 10 i p = 3, to liczba 0.1 będzie miała reprezentację 1.00 × 10
.
Problemy:
ze względu na skończoną dokładność (liczbę bitów) nie wszystkie liczby dadzą się przedstawić
dokładnie
np. dla B = 2 i p = 24 liczba 0.1 będzie miała tylko reprezentację przybliżoną
−4
1.10011001100110011001100 × 2
= 0.0688
Postać ogólna
Liczba zmiennopozycyjna w bazie B i precyzji p będzie miała postać:
e
±d. dd … d × B ,
−1
± (d 0 + d 1 B
−(p−1)
+ ⋯ + d p−1 B
e
) × B ,
0 ≤ di ≤ B
1
−1
Zwróćmy uwagę, że reprezentacja ta nie jest jednoznaczna: liczby 0.01 × 10 i 1.00 × 10
przedstawiają liczbę 0.1 w bazie B = 10 i precyzji p = 3.
Reprezentacja znormalizowana
Aby uczynić reprezentację jednoznaczną, zakłada się często d0
≠ 0:
uzyskana w ten sposób reprezentacja liczb jest rzeczywiście jednoznaczna, ale ...
niemożliwe jest przedstawienie w niej zera w naturalny sposób
najczęściej przyjmuje się, że 0 reprezentowane jest przez 1.00 × Be
min −1
Standard IEEE 754
IEEE 754 (https://pl.wikipedia.org/wiki/IEEE_754 (https://pl.wikipedia.org/wiki/IEEE_754)) to standard
reprezentacji binarnej i operacji na liczbach zmiennoprzecinkowych. Został on ustanowiony w 1985 r przez
Institute of Electrical and Electronics Engineers. Ponieważ rozwiązywał wiele problemów obecnych we
wcześniejszych implementacjach reprezentacji zmiennopozycyjnych, stosowany jest powszechnie we
współczesnych procesorach i oprogramowaniu obliczeniowym.
B = 2 i p = 24 dla liczb w pojedynczej precyzji
B = 2 i p = 53 dla liczb w podwójnej precyzji
w pojedynczej precyzji liczba maszynowa g ma postać:
s
g = (−1)
e−127
∗ 1.m × 2
przesunięcie wykładnika o 127 (w podwójnej precyzji o 1023) powoduje, że do jego przedstawienia
wystarczą liczby dodatnie
w pojedynczej precyzji można przedstawić liczby rzeczywiste z zakresu
38
−3.4 × 10
38
≤ q ≤ 3.4 × 10
w podwójnej precyzji z zakresu
308
−1.8 × 10
308
≤ q ≤ 1.8 × 10
wartości specjalne:
Liczba Reprezentacja
0 0 00000000 00000000000000000000000
−0
1 00000000 00000000000000000000000
∞ 0 11111111 00000000000000000000000
−∞
1 11111111 00000000000000000000000
N aN
0 11111111 00000100000000000000000
N aN
1 11111111 00100010001001010101010
wartość N aN to wynik działań:
∞ + (−∞)
0 × ∞
0/0 , ∞/∞
x
mod 0 , ∞
mod y
√x , x < 0
najmniejsza co do modułu liczba różna od zera w reprezentacji znormalizowanej
−38
±1, 1754 × 10
w pojedynczej precyzji
w podwójnej precyzji
±2, 225 × 10
liczby mniejsze od wartości granicznej w każdej precyzji traktowane jako 0
−308
w celu zwiększenia dokładności obliczeń standard IEEE wprowadza również wartości
zdenormalizowane
wszystkie bity cechy równe 0 i przynajmniej jeden bit mantysy różny od zera
najmniejsza liczba w pojedynczej precyzji to ±1, 40 × 10−45
najmniejsza liczba w podwójnej precyzji to ±4, 94 × 10−324
In [33]:
import sys
In [34]:
sys.float_info
Out[34]:
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_ex
p=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307,
dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, round
s=1)
In [35]:
fmax = sys.float_info.max
In [36]:
print(fmax)
1.7976931348623157e+308
In [37]:
2*fmax
Out[37]:
inf
In [38]:
-2*fmax
Out[38]:
-inf
In [39]:
1/(2*fmax)
Out[39]:
0.0
Przykład ­ reprezentacja liczby 5,375
Przekształcamy liczbę dziesiętną do postaci dwójkowej:
2
5, 375 = 1 ∗ 2
1
+ 0 ∗ 2
0
+ 1 ∗ 2
−1
+ 0 ∗ 2
−2
+ 1 ∗ 2
−3
+ 1 ∗ 2
→ 101.011
Otrzymaną liczbę normalizujemy:
0
101.011 × 2
2
= 1.01011 × 2
Pomijamy wiodącą jedynkę w mantysie: 01011
Obliczamy wykładnik:
2 + 127 = 129 → 10000001
Po określeniu bitu znaku otrzymujemy ostatecznie:
znak wykładnik
mantysa
0
01011000000000000000000
10000001
Typowe problemy w obliczeniach zmiennopozycyjnych
Z omówioną powyżej reprezentacją wiążą się pewne niedokładności w codziennych obliczeniach. Rozważmy
przykład (w C++):
In [40]:
%%writefile reprezentacja.cpp
#include <iostream>
int main()
{
float x=0.01;
if(100.0*x==1.0)
std::cout << "Równe :)\n";
else
std::cout << "Nierówne :(\n";
}
Overwriting reprezentacja.cpp
In [41]:
!g++ reprezentacja.cpp -o rep
In [42]:
!./rep
Nierówne :(
Przyczyną takiego działania programu jest fakt, że liczba 0.01 nie ma dokładnej reprezentacji w standardzie
IEEE, i jest przybliżana najbliższą liczbą maszynową. Podobnie:
In [43]:
%%writefile reprezentacja2.cpp
#include <iostream>
int main()
{
float x=77777.0, y=7.0;
float y1 = 1.0/y;
float z= x/y;
float z1 = x*y1;
if(z==z1)
std::cout << "Równe :)\n";
else
std::cout << "Nierówne :(\n";
}
Overwriting reprezentacja2.cpp
In [44]:
!g++ reprezentacja2.cpp -o rep2
In [45]:
!./rep2
Nierówne :(
Kolejny przykład również związany jest z niedokładną reprezentacją, tym razem liczby 1000, 2:
In [52]:
%%writefile reprezentacja3.cpp
#include <iostream>
int main()
{
float y=1000.2;
float x=y-1000.0;
std::cout << x <<"\n";
}
Overwriting reprezentacja3.cpp
In [53]:
!g++ reprezentacja3.cpp -o rep3
In [54]:
!./rep3
0.200012
Sprawdźmy, czy podobne efekty można zaobserwować w Pythonie:
In [55]:
x = 0.01
In [57]:
100*x == 1.0
Out[57]:
True
In [58]:
x=77777.0; y=7.0
y1 = 1.0/y; z= x/y
z1 = x*y1
In [59]:
z1 == z
Out[59]:
True
In [60]:
y = 1000.2
x = y - 1000.0
In [61]:
print(x)
0.20000000000004547
Na pierwszy rzut oka Python wydaje się lepszy, ponieważ tylko w jednym z trzech przykładów obserwujemy
problem związany z reprezentacją zmiennopozycyjną. Należy jednak zwrócić uwagę, że Python używa liczb
zmiennopozycyjnych o podwójnej precyzji. Natomiast w powyższych przykładach w C++ użyty został tym float o pojedynczej precyzji. Przepisanie tych kodów na typ double, równoważny temu w Pythonie,
prowadzi do następujących wyników:
In [62]:
%%writefile reprezentacjab.cpp
#include <iostream>
int main()
{
double x=0.01;
if(100.0*x==1.0)
std::cout << "Równe :)\n";
else
std::cout << "Nierówne :(\n";
}
Writing reprezentacjab.cpp
In [63]:
!g++ reprezentacjab.cpp -o repb
In [64]:
!./repb
Równe :)
In [65]:
%%writefile reprezentacja2b.cpp
#include <iostream>
int main()
{
double x=77777.0, y=7.0;
double y1 = 1.0/y;
double z= x/y;
double z1 = x*y1;
if(z==z1)
std::cout << "Równe :)\n";
else
std::cout << "Nierówne :(\n";
}
Writing reprezentacja2b.cpp
In [66]:
!g++ reprezentacja2b.cpp -o rep2b
In [67]:
!./rep2b
Równe :)
In [71]:
%%writefile reprezentacja3b.cpp
#include <iostream>
int main()
{
double y=1000.2;
double x=y-1000.0;
std::cout.precision(15);
std::cout << x <<"\n";
}
Overwriting reprezentacja3b.cpp
In [72]:
!g++ reprezentacja3b.cpp -o rep3b
In [73]:
!./rep3b
0.200000000000045
Jak widać, po przejściu na podwójną precyzję w C++ część problemów zniknęła. Otrzymaliśmy wyniki takie
same, jak te w przykładach w Pythonie.
Oczywiście, również w podwójnej precyzji możemy obserwować podobne błędy:
In [76]:
x = 0.1 + 0.1 + 0.1
In [77]:
x == 0.3
Out[77]:
False
In [78]:
print(x)
0.30000000000000004
Jak widać w powyższych przykładach, bezpośrednie porównywanie liczb zmiennopozycyjnych na
komputerze nie zawsze daje oczekiwany wynik. Dlatego w praktyce bardzo często traktować dwie liczby jako
równe, jeżeli moduł ich różnicy jest mniejszy od małej, z góry zadanej wartości:
In [79]:
eps = 1.0e-6
x = 0.1 + 0.1 + 0.1
y = 0.3
In [80]:
x == y
Out[80]:
False
In [81]:
abs(x-y) < eps
Out[81]:
True
Uzyskaliśmy pożądany wynik, jednak ta metoda generuje kilka nowych problemów:
określenie odpowiedniego ϵ nie zawsze jest sprawą oczywistą
relacja zdefiniowana poprzez
a ∼ b ⇔ |a − b| < ϵ
nie jest relacją równoważności!
Jeżeli w programie wykonujemy wiele porównań, wówczas wybieramy wartość ϵ tak, aby określała ona
maksymalny błąd względny, jaki gotowi jesteśmy popełnić przy porównaniu, a następnie porównywać moduł
różnicy nie z samym ϵ, a raczej z iloczynem ϵ i modułu jednej z porównywanych liczb:
In [82]:
eps = 1.0e-6
x = 0.1 + 0.1 + 0.1
y = 0.3
In [83]:
abs(x-y) < eps*abs(y)
Out[83]:
True

Podobne dokumenty