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

Podobne dokumenty