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 FloatingPoint 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, nbitowa liczba całkowita ma wartości od 2^(n1) do 2^(n1) 1. Na maszynach 32bitowych 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 64bitowych 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 8bitowej 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 8bitowej 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