Download: Programowanie_floating_point
Transkrypt
Download: Programowanie_floating_point
PROGRAMOWANIE Przetwarzanie liczb zmiennoprzecinkowych Przetwarzanie liczb zmiennoprzecinkowych Jak to obliczyć? W tym artykule Steven Goodwin zajmuje się problematyką przetwarzania liczb stałoprzecinkowych oraz sposobami wykorzystania ich, w celu poprawienia wydajności aplikacji przeznaczonych dla urządzeń PDA. W artykule omówione zostały różnice między liczbami stało- i zmiennoprzecinkowymi, problemy związane z przenoszeniem oprogramowania między platformami oraz sposoby ich rozwiązywania. STEVEN GOODWIN P ierwotnym powodem wykorzystania liczb zmiennoprzecinkowych w naszym oprogramowaniu były ich matematyczne właściwości. Nie posiadając standardowej biblioteki matematycznej, musimy sami zająć się tworzeniem niezbędnych partii kodu. W ostateczności można jednak zawsze znaleźć w wyszukiwarce Goolge odpowiedni algorytm i dostosować go do potrzeb naszego kodu. Dodaj N do X... Na szczęście nie jest to takie trudne, jakby się mogło wydawać – w końcu jest to niezbyt skomplikowane i dobrze opracowane zagadnienie w zakresie odpowiednich algorytmów. Przeanalizujmy najpierw podstawowe działania matematyczne: dodawanie, odejmowanie, mnożenie i dzielenie. Liczby stałoprzecinkowe są przechowywane w pamięci jako liczby całkowite, dlatego też działania na liczbach stałoprzecinkowych nie różnią się od działań na liczbach całkowitych i nie wymagają tworzenia nowego kodu. #define ( (__a) #define ( (__a) 74 MTH_ADD(__a, __b) U + (__b) ) MTH_SUB(__a, __b) U - (__b) ) Luty 2004 Korzystanie z makr w naszym kodzie może wydawać się zbędne. Jednakże makra można następnie zastąpić wywołaniami funkcji, co umożliwi nam wykrywanie przepełnień, otrzymywanie raportów użycia i uzyskanie pomocy w procesie debuggowania. Nawet przy mnożeniu pojawia się tylko jeden problem i to tylko w przypadku dużych liczb. Istnieje ryzyko (nawet całkiem spore) przepełnienia w trakcie obliczeń części całkowitej liczby stałoprzecinkowej, jednak nawet w takim przypadku wynik końcowy można zapisać na 32 bitach. Aby temu zapobiec, musimy tymczasowo przewymiarować nasz 32-bitowy typ na typ o szerszym zakresie, w celu zapewnienia dokładności obliczeń. Po otrzymaniu wyniku możemy przekonwertować go z powrotem (do FIXP), tracąc jedynie najmniej znaczącą część ułamkową. #define MTH_MUL(__a, __b) U (FIXP)( ((long long)(__a) * U (__b)) > FIX_FRACBITS ) W celu uproszczenia analizy kodu, typ long long będziemy w dalszej części artykułu określać jako FIXP_BIG. W ten sposób system stałoprzecinkowy 8.8 (oparty na typie short int) może używać zwykłego typu long dla www.linux-magazine.pl FIXP_BIG, który jest prostszy w obsłudze i zazwyczaj też szybszy. Oszczędźmy sobie pracy Jak można łatwo zauważyć, nazwy makr definiowane są z prefiksem MTH_, zamiast typowego FIX_. Pozwoli nam to później skompilować ponownie aplikację do działań na liczbach zmiennoprzecinkowych, zmieniając jedynie nazwę makra na: #define MTH_MUL(__a, __b) U ( (__a) * (__b) ) Między notacją stało i zmiennoprzecinkową można przechodzić w taki sam sposób, w jaki włączane i wyłączane są informacje o debugowaniu. To znaczy, skompilować przy pomocy gcc (z argumentem DFIXED_POINT) i wykorzystać poniższy fragment kodu: #ifdef FIXED_POINT #define MTH_MUL(__a, __b) U (FIXP)( ((FIXP_BIG)(__a) * U (__b)) > FIX_FRACBITS ) #else #define MTH_MUL(__a, __b) U ( (__a) * (__b) ) #endif Przetwarzanie liczb zmiennoprzecinkowych Umożliwi to unifikację podstawowego kodu dla obu wersji, który będzie następnie wymagać jedynie minimalnych zmian i poprawek. Jest to ważne, zwłaszcza w przypadku dalszego rozwoju oprogramowania, gdy dodawane są nowe funkcje i coś nie działa. Wtedy można przełączyć się do implementacji zmiennoprzecinkowej i sprawdzić, czy problem tkwi w naszym algorytmie czy też w kodzie stałoprzecinkowym. Kolejne użyteczne rozwiązania Nie tylko mnożenie wymaga naszego nowo zdefiniowanego typu FIXP_BIG. Przyda się on również w operacjach dzielenia. Oba te działania opierają się na tych samych zasadach, co liczby stałoprzecinkowe. Oznacza to, że liczba stałoprzecinkowa zachowuje się jak liczba zmiennoprzecinkowa przemnożona przez stały współczynnik, powiedzmy 100 000. Wszystkie liczby są wtedy 100 000 razy większe niż ich wartości początkowe; uwzględniają one również część ułamkową. Na przykład liczba 14,5 staje się 1 450 000. Jeżeli teraz podzielisz tę liczbę przez 100 000, otrzymasz wynik: 14. Część ułamkowa zostanie utracona, ponieważ działanie zostało wykonane na liczbie całkowitej. Aby zachować część ułamkową (wartość 0,5 zapisaną na pozycjach młodszych od rzędu setek tysięcy!), mnożymy tę liczbę ponownie przez 100 000 (uzyskując w wyniku liczbę 145 000 000 000), co pozwoli zachować część ułamkową po operacji dzielenia. Oczywiście mnożenie dużych liczb wymaga użycia typów danych o odpowiednio dużym zakresie, dlatego zdefiniowaliśmy typ FIXP_BIG. W systemach, w których taki typ nie jest dostępny, należy opracować odpowiedni algorytm mnożenia i dzielenia. Takie przypadki nie zostaną tutaj omówione, ponieważ występują sporadycznie, a metodologia postępowania jest powszechnie znana. FIXP fixDiv(FIXP num, U FIXP divisor) { FIXP_BIG tmp = (FIXP_BIG)num; tmp <<= FIX_INTBITS+U FIX_FRACBITS; tmp /= divisor; tmp >>= FIX_INTBITS; return (FIXP)tmp; } Relacje typu większe niż oraz mniejsze niż będą bezpośrednio wykorzystywać operandy > oraz <. Powyższe działania uwzględniają także liczby ujemne. Skoro liczby te już istnieją PROGRAMOWANIE w postaci stałoprzecinkowej, nie ma potrzeby przekazywania ich przez makra, ponieważ potencjalne ryzyko przepełnienia nie istnieje. kod jest przypisany do określonego formatu, przykładowo 16.16. I kolejny krok naprzód... Funkcja inicjalizująca jest również bardzo przydatna w następnym omawianym zagadnieniu funkcjach. Jeden z największych obszarów biblioteki math poświęcony jest funkcjom trygonometrycznym, takim jak sinus, cosinus i tangens. Są one zwykle obliczane popularnymi narzędziami matematycznymi, na przykład za pomocą szeregu Taylora. Zachowanie odpowiedniej dokładności z wykorzystaniem mechanizmów stałoprzecinkowych wymagałoby dużej mocy obliczeniowej i w praktyce mogłoby generować wąskie gardła wydajności. Aby zaoszczędzić moc obliczeniową, funkcje te nie są obliczane. Ponieważ funkcja sinus (jak również i cosinus) są okresowe (to znaczy, ich wartości powtarzają się w ściśle określonych interwałach), możemy stworzyć prostą tablicę odwołań dla tych interwałów i odwoływać się bezpośrednio do niej. Tablice odwołań mogą być także używane dla funkcji arc-sin i arc-cos, które także mieszczą się w określonym zakresie. Funkcja fixInitialiseLibrary może utworzyć tablicę składającą się z 1024 elementów za pomocą (powolnej) arytmetyki zmiennoprzecinkowej i wykorzystywać ją w dalszej części programu. Zobacz Listing 2. Rozmiar tej tablicy można dowolnie określać, w zależności od ilości pamięci, która ma być wykorzystana w tym celu oraz wymaganego stopnia dokładności obliczeń narzuconego przez konkretne zastosowanie, jednak tablica składająca się z 1024 elementów wydaje się być wystarczająca w przypadku większości zastosowań. Metoda tablicy odwołań znajduje również zastosowa- W zależności od zastosowania może zajść potrzeba użycia stałych matematycznych, takich jak Pi oraz e. Oczywiście biblioteka maths nie zawiera ich w postaci stałoprzecinkowej, trzeba je zatem stworzyć samodzielnie. pi = FLOAT_TO_FIXED U (3.14159265358979f); Możemy również użyć predefiniowanych stałych M_PI oraz M_E zawartych w /usr/include/math.h, ponieważ ich dokładność jest większa. pi = FLOAT_TO_FIXED(M_PI); Jest to dobry przykład sytuacji, w której funkcja fixInitialiseLibrary ma zastosowanie. W trakcie uruchamiania programu procedura inicjalizacji definiuje zestaw stałych, takich jak Pi oraz e, do których kod odwołuje się bezpośrednio, bez dodatkowego obciążania procesora. Zobacz Listing 1. Ponownie użyliśmy nazw z prefiksem MTH_, umożliwiających tworzenie kodu dla działań na liczbach zmiennoprzecinkowych przy minimalnym nakładzie pracy. Stworzyliśmy specjalną stałą 2pi (rezygnując z używania stałej g_MthConstant_PI*2), poprawiając w ten sposób dokładność o jeden rząd dziesiętny. W niektórych bibliotekach stałoprzecinkowych wartość stałej pi jest definiowana bezpośrednio w kodzie, bez wykorzystania funkcji inicjalizującej. Jest to poprawne, jednak taki Szybciej z tablicą odwołań Listing 1: Ustawianie stałych. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 /* fixed.c */ FIXP g_MthConstant_PI, g_MthConstant_2PI, g_MthConstant_E; void fixInitialiseLibrary(void) { g_MthConstant_PI = FLOAT_TO_FIXED(3.1415926535f); g_MthConstant_2PI = FLOAT_TO_FIXED(6.2831853071f); g_MthConstant_E = FLOAT_TO_FIXED(2.7182818285f); } /* fixed.h */ #define MTH_PI #define MTH_2PI #define MTH_E g_MthConstant_PI g_MthConstant_2PI g_MthConstant_E extern FIXP g_MthConstant_PI, g_MthConstant_2PI, g_MthConstant_E; www.linux-magazine.pl Luty 2004 75 Przetwarzanie liczb zmiennoprzecinkowych PROGRAMOWANIE nie dla funkcji cosinus, ma ona identyczny przebieg jak funkcja sinus, jest jedynie przesunięta w fazie o pi/2 radiana. FIXP fixCos(FIXP theta) { return fixSin(theta + U MTH_PI/2); } Powyższa tablica odwołań może zostać udoskonalona przez interpolacje wartości niezdefiniowanych w tabeli. Jeżeli nie chcesz korzystać z tabeli odwołań, wartości funkcji można wyznaczać przez aproksymację szeregu Taylora lub algorytmu Remeza. Metody wyznaczania wartości innych funkcji, takich jak tangens, można znaleźć w dobrych podręcznikach do matematyki. Koszt Niektóre firmy, takie jak Ekkla Research, dostarczają biblioteki dla liczb stałoprzecinkowych, które można następnie połączyć w kodzie z biblioteką GNU dla liczb zmiennoprzecinkowych. Po zastosowaniu takiego rozwiązanie pozostaje już tylko ponowne skompilowanie całości. Niestety za tę bibliotekę (oraz niektóre inne biblioteki) trzeba zapłacić, dlatego przed rozpoczęciem pracy warto się zastanowić, co jest ważniejsze czas czy też pieniądze. Grupą funkcji, która nie może być przedstawiona w postaci prostej tabeli odwołań (jak sinus), są funkcje nieokresowe (takie jak funkcje potęgowe) oraz takie, które nie przyjmują dwóch jednakowych wartości w całej swojej dziedzinie (nieograniczonej): np. pierwiastek kwadratowy. Metoda tabeli odwołań nie sprawdzałaby się dla takich klas funkcji, dlatego też należy zaimplementować kompletny algorytm obliczeniowy lub przynajmniej zastosować metodę aproksymacyjną. Przed implementacją algorytmu, która wymaga czasu potrzebnego na jego opracowanie i mocy obliczeniowej proceso- ra, należy odpowiedzieć na następujące pytania: Czy na pewno jest to potrzebne? Jeżeli funkcja ma określić odległość pomiędzy dwoma punktami, należy skorzystać z twierdzenia Pitagorasa. Jednak gdy trzeba tylko określić, który z dwóch punktów znajduje się bliżej, wykorzystanie funkcji pierwiastkowej wydaje się bezzasadne. Czy wymagana jest aż tak duża dokładność obliczeń? Pytanie to nie jest tak śmieszne, jak mogłoby się to na pierwszy rzut oka wydawać. Wiele formatów plików wykorzystuje algorytmy stratne (JPEG, MP3), dlatego przetwarzanie też może być takiego typu. We wspomnianym przykładzie określania odległości pomiędzy dwoma punktami, dobrą aproksymacją byłaby suma długości dłuższego i krótszego odcinka. Czy można ponownie napisać algorytm w celu wyeliminowania funkcji lub użyć innej funkcji w mniejszym stopniu obcią- 01 /* fixed.c - compile with -lm option to link in maths library for sin() fn */ 02 #include "math.h" 03 04 static FIXP g_SineTable[1024]; 05 06 void fixInitialiseLibrary(void) 07 { 08 ... 09 10 for(i=0;i<1024;i++) 11 { 12 fSineValue = sin(M_PI * 2.0f * i / 1024.0f); 13 g_SineTable[i] = FLOAT_TO_FIXED(fSineValue); 14 } 15 16 } 17 18 FIXP fixSin(FIXP theta) 19 { 20 /* We need to scale theta from 0 to 2pi to 0 to 1023 */ 21 /* i.e. offset = theta/2pi * 1024 */ 22 23 /* Compute the scale as a float */ 24 float scale = 1024.0f / (2.0f * M_PI); 25 26 /* Convert to our fixed-point notation */ 27 FIXP fixed_scale = FLOAT_TO_FIXED(scale); 28 29 /* Find our genuine offset */ 30 int offset = MTH_MUL(theta, Sprawa staje się bardziej złożona żającej moce obliczeniowe procesora? Jeśli tak, zrób to. Jeżeli zaimplementowanie funkcji stałoprzecinkowej dla przykładowo funkcji pierwiastka kwadratowego okaże się konieczne, należy wtedy zastosować algorytm skalowalny. W matematyce większość algorytmów opiera się na iteracji – ta sama operacja jest wykonywana tyle razy, aż błąd stanie się na tyle mały, iż można go uznać za nieznaczący. Implementując taki algorytm, można zmniejszyć liczbę iteracji i dostosować ją do mocy obliczeniowej procesora. W poniższym przykładzie dla funkcji pierwiastkowej drugiego stopnia zastosowano cyfrę 8. Można łatwo się przekonać, o ile większą precyzję gwarantuje funkcja, ile trwa przetworzenie danych oraz wybrać optymalne wartości dla określonego zastosowania. Patrz Listing 3. Przykłady algorytmów tego typu można znaleźć pod adresem [2]. Bardziej złożone algorytmy, dla funkcji takich jak logarytmy naturalne, można znaleźć w [3]. Inne problemy Jest jeszcze kilka innych funkcji, które mogą okazać się potrzebne. Ich zastosowanie nie jest zbyt skomplikowane, ponieważ korzystają one z dobrze znanych algorytmów. Tak jak już wspomniałem, należy implementować tylko to, co jest naprawdę potrzebne. Patrz Listing 4. Ten przykład przedstawia jeden z najczęstszych problemów występujących przy programowaniu bibliotek matematycznych: liczby ujemne mają inne cechy niż liczby dodatnie, zwłaszcza w przypadku zaokrąglania. Przy Listing 2: Plik fixed.c 76 Luty 2004 www.linux-magazine.pl fixed_scale); 31 32 /* Convert out fixed-point number into a 'normal' int */ 33 offset >>= FIX_FRACBITS; 34 35 /* Sine is a cyclic function, so make sure an offset of 1024 36 maps to 0, 1025 map to 1, and so on. 37 (A table of 1024 elements was chosen to make masking easy) */ 38 offset = offset & 1023; 39 40 /* Give our result back to the world */ 41 return g_SineTable[offset]; 42 } Prenumerata Linux Magazine Nie przegap takiej okazji! Zamawiając prenumeratę oszczędzasz! Płacisz jak za 9 numerów, a otrzymujesz 12! Z każdym numerem DVD lub płyta CD-ROM. Najszybszy sposób zamówienia prenumeraty: http://www.linux-magazine.pl Infolinia: 0801 800 105 PROGRAMOWANIE Przetwarzanie liczb zmiennoprzecinkowych tworzeniu kodu biblioteki, nie da się niestety uniknąć podobnych trudności. Dodatkowo można jeszcze zaimplementować funkcję odwrotną, działającą szybciej niż operacja 1/N oraz niestandardowy mechanizm fixPrint, który nie powoduje utraty precyzji przy konwersji. Podczas pisania i testowania kodu na pewno dokonasz jeszcze wielu spostrzeżeń. Szybkość Po przeniesieniu kodu do nowej, szybszej notacji stałoprzecinkowej może się okazać, iż szybkość działania aplikacji nadal pozostawia nieco do życzenia. Oprócz standardowych technik optymalizacyjnych, warto zapoznać się także z mechanizmami dotyczącymi tylko liczb stałoprzecinkowych. Pierwszy krok ku zwiększeniu szybkości polega na zmianie kolejności wyrażeń (np. a*b/c) w taki sposób, aby stałe liczbowe znajdowały się w jednej grupie. Należy także rozważyć obliczanie ich osobno podczas odwoływania się do tabeli sinusów. Jeżeli ta część aplikacji najbardziej spowalniała jej działanie, a w szczególności wyrażenie n/2pi * 1024 Można je sformułować w poniższej postaci: n * (1024/2pi) Po obliczeniu wartości stałej 1024/2pi, można jej użyć zamiast całego wyrażenia, co pozwala jeszcze zwiększyć szybkość. Stałą tę można następnie umieścić w pliku fixInitialiseLibrary. Metoda ta sprawdza się we wszystkich operacjach wymagających dzielenia. Działanie to jest wykonywane wolno, zarówno w przypadku liczb stało, jak i zmiennoprzecinkowych. Zatem, gdzie tylko jest to możliwe, działanie podziel przez N należy zastąpić działaniem pomnóż przez 1/N. Wartość 1/N powinna być już Listing 3: Funkcja pierwiastkowa dla liczb stałoprzecinkowych. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 FIXP fixSqrt(FIXP num) { FIXP Unity = 1<<FIX_FRACBITS; FIXP tmp = num + Unity; int i; tmp >>= 1; /* Divide by two */ for(i=0;i<8;i++) { tmp = tmp+fixDiv(num,tmp); tmp >>= 1; } return tmp; } wstępnie obliczona, tak jak w powyższym przykładzie, co znacznie przyspieszy działanie aplikacji. Można to zrobić przy użyciu specjalnej funkcji odwrotnej, co zostało omówione w poprzedniej sekcji. Warto również przeanalizować szczególne przypadki. Jeżeli szybkość algorytmu opiera się na działaniu podziel przez 2, a teraz wprowadzamy zamiast niego działanie pomnóż przez 0,5, należy dla nowego działania napisać specjalny kod. W tym przypadku użyjemy sposobu już raz wykorzystanego dla funkcji pierwiastkowej drugiego stopnia. iHalfOfN = N >> 1; Jeżeli N jest liczbą całkowitą, wtedy niezależnie od użytego formatu liczb stałoprzecinkowych (12.20 lub 16.16), operacja dzielenia zostanie wykonana bardzo szybko. Tak samo dzieje się przy dzieleniu oraz mnożeniu przez 2 lub dwolną liczbę będącą jej wielokrotnością Listing 4: Inne funkcje. 01 FIXP fixRound(FIXP num) 02 { 03 FIXP round, trunc; 04 05 if (num & (1<<(FIX_FRACBITS-1))) 06 round = 1<<FIX_FRACBITS; 07 else 08 round = 0; 09 10 if (num > 0) 11 { 78 Luty 2004 12 13 14 15 16 17 18 19 20 trunc = num & ~((1<<FIX_FRACBITS)-1); return trunc + round; } else { trunc = (-num) & ~((1<<FIX_FRACBITS)-1); return -trunc + round; } } www.linux-magazine.pl (4,8,16,32,64). Metoda ta nie sprawdza się w przypadku liczb zmiennoprzecinkowych, nie ma to jednak w tym przypadku znaczenia – pracujemy na liczbach stałoprzecinkowych i szukamy rozwiązań dla nich najlepszych. Nawet, jeśli na pierwszy rzut oka wydają się być one dziwne. Jako przykład może tutaj służyć implementacja funkcji fixRound. Podobną metodę przemnażania przed wykonaniem zasadniczej operacji można zastosować także dla innych stałych. Operację pomnóż przez 3 można zoptymalizować, szacując wartość stałoprzecinkową cyfry 3 (np. przy użyciu FLOAT_TO_FIXED(3)) i mnożąc bezpośrednio przez zmienną. iThreeTimesBigger = U fixed_point_number * to_fixed3; Oczywiście zawsze można pokusić się o samodzielne napisanie kodu w asemblerze. Decyzja o zastosowaniu liczb stałoprzecinkowych jest zwykle podyktowana sprzętem i dużym naciskiem kładzionym na szybkość pracy, dlatego też można utworzyć makro (USE_STRONGARM_ASM) i dołączyć do niego specjalny kod napisany w asemblerze. Na sam koniec jedna uwaga odnośnie makra FLOAT_TO_FIXED. Niektóre procesory bardzo wolno konwertują liczby całkowite na zmiennoprzecinkowe i odwrotnie. Jeżeli takie konwersje są powtarzane w ciasnych pętlach, zalecam wyciągnięcie konwersji i obliczenie ich w taki sposób, jak były obliczne stałe z biblioteki maths. W razie wątpliwości można ucieć się do przemnażania. I na koniec... W artykule przedstawiono wiele wiadomości i metod umożliwiających zastosowanie wydajnego oprogramowania na małych platformach, dla których należy opracowywać indywidualne rozwiązania dostosowane do określonych potrzeb. Przykładowy kod zaprezentowany w artykule można znaleźć pod adresem [4], w katalogu sources. ■ INFO [1] Floating Point Processing – Numbers everywhere! Linux Magazine, nr 38 styczen 2004, str. 65. Artykul ten ukaze sie w polskiej wersji jezykowej za miesiac. [2] Przykladowe algorytmy: http://www.ai.mit.edu/people/hqm/imode/ fplib/cordic_code.html [3] Donald Knuth, The Art of Computer Programming [4] Kod zródlowy do artykulu: http://www.bluedust.com/pub