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

Podobne dokumenty