Arytmetyka liczb wymiernych w j˛ezyku C++
Transkrypt
Arytmetyka liczb wymiernych w j˛ezyku C++
Arytmetyka liczb wymiernych w j˛ezyku C++ Monika Zagała Wydział Inżynierii Mechanicznej i Informatyki Kierunek Informatyka, Rok V [email protected] Streszczenie Poniższa praca przedstawia projekt oraz implementacj˛e nowego typu danych mzRational dla j˛ezyka C++, służacego ˛ do prostych operacji arytmetycznych na liczbach wymiernych. Artykuł wykazuje jego słabe i mocne strony, na podstawie porównania z liczbami zmiennoprzecinkowymi (dost˛epnymi w standardzie j˛ezyka), precyzji wyników otrzymanych dla prostych działań matematycznych. Ponadto, została zaproponowana arytmetyka mieszana mi˛edzy liczbami wymiernymi, a liczbami zmiennopozycyjnymi oraz omówione zostały problemy, jakie si˛e z tym wiaż ˛ a.˛ 1 Wst˛ep Wraz, z pojawieniem si˛e pierwszych maszyn liczacych, ˛ czynności zwiazane ˛ z pobieraniem i przetwarzaniem danych liczbowych, zostały zautomatyzowane. Do wykonywania działań arytmetycznych stosowany jest powszechnie typ zmiennopozycyjny. Niestety, w wielu przypadkach, obliczenia wykonywane przy jego pomocy, daja˛ przybliżone rezultaty. Wyst˛epujace ˛ bł˛edy, sa˛ spowodowane min. brakiem skończonego rozwini˛ecia dziesi˛etnego, dla niektórych ułamków zwykłych [1]. Inna˛ istotna˛ sprawa˛ jest kolejność wykonywania działań. Ma ona duży wpływ na precyzj˛e otrzymywanych wyników [2]. Fakt, że reprezentacja liczb zmiennoprzecinkowych w pami˛eci komputera nie zawsze jest precyzyjna, nasuwa ide˛e zastosowania zamiennie danych, w postaci wymiernej. Dzi˛eki temu, że sa˛ one przedstawiane za pomoca˛ pary liczb: licznika i mianownika, uniknać ˛ można np. bł˛edów zaokraglenia ˛ liczb, które towarzysza˛ postaci dziesi˛etnej implementacji. Ze wzgl˛edu na brak ogólnodost˛epnego typu liczb wymiernych dla j˛ezyka C++ oraz przez wzglad ˛ na jego duże zapotrzebowanie w wielu dziedzinach nauki i techniki, została zaprojektowana biblioteka zawierajaca ˛ zestaw algorytmów i funkcji, umożliwiajacych wykonywanie operacji arytmetycznych, na liczbach reprezentowanych w postaci ułamków zwykłych. Praca zorganizowana jest w nastepujacy ˛ sposób: W rozdziale drugim przedstawiona została reprezentacja liczb zmiennoprzecinkowych, wraz z charakterystyka˛ najcz˛eściej spotykanych bł˛edów wystepujacych ˛ w obliczeniach. Rozdział trzeci zawiera definicj˛e typu mzRational oraz jego porównanie z typami zmiennopozycyjnymi, na podstawie prostych przykładów działań arytmetycznych. Rozdział czwarty określa zasady arytmetyki mieszanej opracowanego typu liczb wymiernych, z istniejacymi ˛ postaciami reprezentacji liczb rzeczywistych. 1 2 Reprezentacja liczb zmiennoprzecinkowych Liczba zmiennoprzecinkowa (ang. floating–point number) służy do przedstawienia ograniczonego przedziału liczb rzeczywistych w pami˛eci komputera. Wszystkie założenia, zwiazane ˛ z reprezentacja˛ tego typu, zdefiniowane zostały przez standard IEEE 754 [3]. W praktyce stosowane sa˛ trzy metody wyświetlania liczb zmiennoprzecinkowych: dziesi˛etna, naukowa oraz inżynierska. Najbardziej popularnym zapisem jest notacja naukowa[4]. Stosujac ˛ dynamiczne przesuni˛ecie przecinka oraz używajac ˛ pot˛egi podstawy do określenia jego rzeczywistego położenia, możemy reprezentować dowolne liczby za pomoca˛ kilku cyfr [5]. Ogólny wzór wyglada ˛ nast˛epujaco: ˛ zm M ∗ βzcC (1) gdzie : M – mantysa liczby (ang. mantissa) , C – cecha (ang. exponent) , β – używana podstawa systemu liczbowego (ang. base) , zm – znak mantysy, zc – znak cechy. Zarówno, dla mantysy, jak i wykładnika ilość cyfr jest z góry ustalona. Zatem, dana liczba jest reprezentowana, z pewna˛ skończona˛ dokładnościa˛ i należy, do policzalnego zbioru wartości [2]. Przy obliczeniach, wykonywanych na liczbach zmiennopozycyjnych, można napotkać podstawowe rodzaje błedów : • Bł˛edy danych wejściowych – wyst˛epuja˛ wówczas, gdy dane liczbowe wprowadzone do pami˛eci, lub rejestrów maszyny cyfrowej, odbiegaja˛ od dokładnych ich wartości. • Bł˛edy reprezentacji – problem isnieje, w przypadku reprezentacji wszystkich liczb √ niewymiernych np. Π, 3, liczb o nieskończonym rozwini˛eciu dziesi˛etnym np. 1/3, 1/6, 1/7 oraz dla ułamków dziesi˛etnych o nieskończonym rozwini˛eciu binarnym np. 0.1, 0.2. Nieuniknione jest wówczas zaokraglenie. ˛ • Bł˛edy obci˛ecia – powstaja˛ podczas obliczeń, na skutek zmniejszania liczby działań. Na przykład, podczas dodawania bardzo małej i bardzo dużej liczby, ze wzgl˛edu na ograniczona˛ reprezentacj˛e mantysy wyniku, jej przesuni˛ecie wzgl˛edem tych samych cech, powoduje brak dodania liczb, a otrzymanym wynikiem b˛edzie wartość liczby wi˛ekszej. • Bł˛edy zaokragleń ˛ – pojawiaja˛ si˛e podczas obliczeń, na skutek konieczności zaokraglania ˛ wartości, ze wzgledu na ograniczona˛ długość słów binarnych. Bł˛edy te można czasem zmniejszyć, ustalajac ˛ umiej˛etnie sposób i kolejność wykonywania działań. Liczb˛e zmiennoprzecinkowa˛ można potraktować, jako sum˛e wartości dokładnej oraz poprawki do wartości liczby zmiennoprzecinkowej [3]: f =d+p (2) gdzie: f – wartość zmiennoprzecinkowa; d – wartość dokładna, która˛ reprezentuje liczba f ; p – poprawka wartości d do wartości f , zwana również bł˛edem zaokraglenia ˛ (może przyjmować wartości dodatnie oraz ujemne). Dla przykładu, liczby: 2 float d1 = 123456.78 float d2 = 103.6003 sa˛ reprezentowane jako: f 1 = d1 + p1 i f 2 = d2 + p2, przy czym: f1 = 123456.781250, f2 = 103.600304 natomiast bł˛edy zaokraglenia ˛ wynosza˛ odpowiednio: p1 = 0.00125 , p2 = -0.000004. Podczas dodawania dwóch liczb zmiennoprzecinkowych mamy do czynienia, z sumowaniem si˛e bł˛edów: f 1 + f 2 = d1 + p1 + d2 + p2 = (d1 + d2) + (p1 + p2); | {z } bład ˛ (3) Jeżeli, poprawki: p1 i p2 maja˛ przeciwne znaki, wówczas bład ˛ może być nieco mniejszy. Teoretycznie, po podstawieniu do wzoru liczb otrzymamy: f = 123560.381554 d = 123560.3803 p = 0.001254 Wyniki otrzymane, przy użyciu kompilatora dla j˛ezyka C++, różnia˛ si˛e od przedstawionych wyżej, gdyż dochodza˛ jeszcze bł˛edy reprezentacji poszczególnych składników działań arytmetycznych oraz otrzymanego wyniku. Stad, ˛ f = 123560.382812, natomiast poprawka p = 0.002512. Mnożenie dwóch liczb zmiennoprzecinkowych, przedstawia poniższe równanie: f 1 ∗ f 2 = (d1 + p1) ∗ (d2 + p2) = (d1 ∗ d2) + (d1 ∗ p2 + d2 ∗ p1 + p1 ∗ p2); {z } | bład ˛ (4) Dodajac, ˛ do wartości uj˛etej w nawias klamrowy (z wzoru (4) ), bł˛edy numeryczne, wynikajace ˛ z niedokładnej reprezentacji tych liczb, uzyskany bład ˛ całkowity może być duży. Biorac ˛ pod uwag˛e, że jest to jedynie pojedyncza operacja, warto zastanowić si˛e, kiedy dokonywanie bardziej skomplikowanych operacji arytmetycznych ma w ogóle sens [2]. 3 Działania arytmetyczne na liczbach wymiernych Z poprzedniego rozdziału wynika, że typ zmiennopozycyjny niesie ze soba˛ wiele niedoskonałości. Można łatwo uzyskać bezużyteczne wyniki, czyli takie, które obarczone sa˛ bardzo dużym bł˛edem. Zastosowanie wi˛ekszej precyzji liczb zmiennoprzecinkowych, jest jedna˛ z metod osłabiajac ˛ a˛ ryzyko uzyskania niedokładnych wyników [2]. Jednak, w wielu laboratoriach naukowych, technicznych, czy przemysłowych, gdzie jakość obliczeń ma bardzo duże znaczenie, arytmetyka zmiennopozycyjna może okazać si˛e zawodna. Fakt ten, przyczynił si˛e do prac nad nowym typem danych zwanym ogólnie Rational. Głównym założeniem jest przedstawienie liczb rzeczywistych, wymiernych, za pomoca˛ ułamków zwykłych. Licznik i mianownik sa˛ zapisywane w postaci liczb całkowitych, i dlatego podstawowe działania matematyczne wykonywane sa˛ z pełna˛ precyzja.˛ Na przykład dla j˛ezyków takich jak: Java, czy Python istnieja˛ odpowiedniki takiej biblioteki. 3 Na stronie internetowej Boost’a [6] można znależć implementacj˛e typu rational dla j˛ezyka C++, wraz z podstawowymi algorytmami i funkcjami. Brakuje jednak operandów dla arytmetyki mieszanej i możliwosci rzutowania typu zmiennopozycyjnego, na typ wymierny. Pakiet ten jest biblioteka˛ "otwarta", ˛ wciaż ˛ opracowywana.˛ Na jego podstawie została zaprojektowana własna biblioteka mzRational, z operandami: dodawania, odejmowania, mnożenia i dzielenia, a także relacji porównania. Dodatkowo zostały przeciażone ˛ operatory typów zmiennoprzecinkowych oraz zdefionowana została ich konwersja do mzRational, wraz z funkcjami dla całej arytmetyki mieszanej. Na podstawie przykładowych dwóch liczb: a = 1223334444.5 i b = 2e-8 zostało dokonane porównanie operacji dodawania i mnożenia pomi˛edzy danymi typu mzRational, gdzie poszczególne składowe ułamka zwykłego zdefiniowane zostały jako long long int oraz liczbami zmiennoprzecinkowymi typu double. Otrzymane wyniki były nast˛epujace: operacja mzRational double + (61166722225000001/50000000) 1223334444.500000000000000000 ∗ (2446668889/100000000) 24.4666890000000356 dla wymiernej reprezentacji rezultat był prawidłowy, zarówno dla operacji dodawania oraz mnożenia. W przypadku liczb zapisanych w postaci dziesietnej, operacja dodawania dała wynik równy wi˛ekszemu czynnikowi, czyli wystapił typowy bład ˛ obci˛ecia, charakterystyczny dla tego typu danych. Natomiast mnożenie zostało przeprowadzone precyzyjnie, z niewielkim bł˛edem reprezentacji. Nasuwa si˛e tutaj wniosek, że typ mzRational wykazuje zdecydowana˛ przewag˛e nad typem zmiennopozycyjnym, w operacjach dodawania (odejmowania) liczb skrajnie różnych. Porównanie arytmetyki, dla dwóch innych liczb: c = 45e12 oraz d = 5e-8, wykazało, że mnożenie wykonane zostało prawidłowo na liczbach mzRational, natomiast dodawanie zakończyło si˛e bł˛edem spowodowanym przekroczeniem najwyższej wartości liczby typu long long int. Podobnie, dla operacji mnożenia może wystapić ˛ overflow (underflow), czyli tzw. bład ˛ nadmiaru (niedomiaru), szczególnie wtedy, gdy redukcja ułamków zwykłych jest niewykonalna. Zatem problem zachowania precyzji nie jest do końca rozwiazany. ˛ W tym konkretnym przypadku widoczna jest wyższość typów zmiennopozycyjnych typu double(long double). Przy zastosowaniu liczb typu float sprawa przedstawia si˛e inaczej. Porównanie zakresów możliwych prezentowanych wyników wypada na korzyść reprezentacji mzRational. Dodatkowym atutem, reprezentacji liczb w postaci wymiernej, jest łatwość ich porównywania. Powszechnie wiadomo, że takie operacje na liczbach prezentowanych w postaci ułamków dziesi˛etnych nie sa˛ możliwe. Można jedynie sprawdzić, czy dana liczba zmiennopozycyjna mieści si˛e w pewnym jej zakresie, otoczeniu [3]. Typ mzRational zapewnia nam operatory (<, >, ==, ! =) dla tego typu relacji, zwracajace ˛ odpowiednio true, jeżeli została ona spełniona, w przeciwnym razie false. Poniżej znajduje si˛e fragment implementacji operatora mniejszości: template<typename Int> bool mzRational<Int>::operator<( const mzRational<Int>& less){ mzRational<Int> l(*this); mzRational<Int> r(less); 4 if(l.num < 0 && r.num >= 0) return true; if(l.num >= 0 && r.num <= 0) return false; if(l.den == r.den) return l.num < r.num; l.normalize(); r.normalize(); Int gcd1 = gcd(l.num, r.num); Int gcd2 = gcd(r.den, l.den); return (l.num/gcd1) * (r.den/gcd2) < (l.den/gcd2) * (r.num/gcd1); } Funkcja normalize() służy do redukcji ułamków zwykłych, natomiast gcd(), jako rezultat zwraca najwi˛ekszy wspólny dzielnik. Zastosowanie operatora < wymaga zdefiniowania dwóch obiektów typu mzRational i porównaniu ich ze soba.˛ Ilustruje to poniższy przykład: int main(){ mzRational<long int> a(12, 78); mzRational<long int> b(34, 13); if(a < b){...} return 0; } Inna˛ cecha˛ typu mzRational, jest reprezentacja wyników w postaci zredukowanych ułamków zwykłych. Notacja dziesi˛etna jest zdecydowanie bardziej przyswajalna, od tego rodzaju prezentacji danych. Na przykład, liczba a = 1223334444.5 zostanie przedstawiona odpowiednio przez typ zmiennopozycyjny jako: 1223334444.500000000000000000 natomiast mzRational wyświetli si˛e jako: (2446668889 / 2) T˛e mała˛ niedogodność rekompensuje możliwość zamiany typu z mzRational na dowolny typ zmiennoprzecinkowy. Trzeba si˛e liczyć z tym, że w niektórych przypadkach, konwersja może przyczynić si˛e, do utraty dokładności prezentowanej liczby. Porównanie typów: mzRational ze wszystkimi typami zmiennopozycyjnymi nie miało na celu wykazania, który z nich jest lepszy. Zarówno jedna, jak i druga reprezentacja niesie ze soba˛ wiele zalet i wad. Jednakże, wykazanie słabych i mocnych stron pomaga w dobraniu odpowiedniego typu, w zależności od wykonywanych operacji. 4 Definicja arytmetyki mieszanej Najważniejszym, a zarazem najtrudniejszym zagadnieniem jest arytmetyka liczb mieszanych, czyli określenie zasad działania na liczbach wymiernych typu mzRational, z liczbami zmiennopozycyjnymi w dowolnym formacie. Stosowanie zamiennie liczb zmiennopozycyjnych i wymiernych wymaga zdefiniowania operatorów rzutowania: operator float( ){...} operator double( ){...} operator long double( ){...} do zamiany typu mzRational, na jeden z powyższych typów zmiennopozycyjnych oraz zdefiniowania konwersji odwrotnej, czyli liczby rzeczywistej w dowolnej reprezentacji zmiennoprzecinkowej na liczb˛e mzRational: 5 template<typename Real> explicit mzRational(Real x){...} Rzutowaniu ułamków, z postaci zwykłej na postać dziesietna,˛ towarzyszy cz˛esto utrata precyzji. Jest to zwiazane ˛ przede wszystkim z bł˛edami w reprezentacji zmiennoprzecinkowej. Odwrotna zamiana typów również nie pozwala uniknać ˛ bł˛edów. Przyczyny tego moga˛ być nast˛epujace. ˛ Po pierwsze, zamieniana liczba zmiennoprzecinkowa nie mieści si˛e w granicach reprezentacji liczby wymiernej, wówczas konieczne jest obci˛ecie, badź ˛ zaokraglenie ˛ liczby do n–cyfr (w jezyku C++, liczby typu long long int sa˛ zazwyczaj, co najwyżej 18–cyfrowe). Drugi rodzaj bł˛edu, z jakim można si˛e spotkać przy konwersji liczb rzeczywistych do typu mzRational, wynika z niedokładnej reprezentacji liczby zmiennoprzecinkowej. Ostatnim powodem utraty precyzji jest zamiana liczb niewymiernych na postać wymierna.˛ Tego typu dane nigdy nie zostana˛ poprawnie przedstawione, co wynika z własności tych liczb [7]. Faktem jest, że nie każda zamiana typów spowoduje, że wartości liczbowe utraca˛ swoja˛ pierwotna˛ dokładność. Jednak świadomość tego, kiedy i gdzie sa˛ popełniane bł˛edy ułatwia określenie zasad dodawania, odejmowania, mnożenia i dzielenia liczb mieszanych, w taki sposób, by osiagn ˛ ać ˛ jak najwyższa˛ prezyj˛e otrzymywanych wyników. 5 Podsumowanie Artykuł przedstawia ogólna˛ charakterystyk˛e typu mzRational. Liczby prezentowane, jako ułamki zwykłe, poszerzaja˛ dotychczasowe możliwości, o wykonywanie precyzyjnego dodawania (a co za tym idzie, odejmowania) liczb, szczególnie o dużej rozbieżności wykładników. Ponadto, łatwość wykonywania porównań, takich jak: która z liczb jest wi˛eksza, badź: ˛ czy dwie liczby sa˛ równe, czy różne - to dodatkowy atut typu mzRational. Niestety, każdy reprezentacja danych, w pami˛eci komputera posiada pewne wady. Tak też jest w przypadku reprezentacji wymiernej implementacji. Świadcza˛ o tym wyżej przedstawione przykłady. Dzi˛eki poznaniu i zrozumieniu wszelkich ograniczeń, zastosowanie w konkretnych aplikacjach arytmetyki liczb wymiernych, staje si˛e o wiele prostsze i bardziej efektywne. Literatura [1] W. Hebish, A. Szustalewicz, K. Tabisz, Wst˛ep do informatyki, 2002. [2] D. Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, 1991. [3] K. Adamski, http://nr-k.namyslow.eu.org/, Liczby zmiennoprzecinkowe, 2003. [4] Wikipedia, http://pl.wikipedia.org/wiki/Liczba_zmiennoprzecinkowa. [5] P. Furmański, Ś. Sobieski, Wst˛ep do Informatyki, wer. RCI, 2004. [6] Boost, http://www.boost.org/libs/rational. [7] T. Trajdos, Matematyka, wyd. VI, 1993. 6