|
|
Linia 1: |
Linia 1: |
| | |
| ''Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzi? poprawki'' | | ''Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzi? poprawki'' |
|
| |
| ==Rozwiązywanie układów równań liniowych==
| |
|
| |
| Jest to algorytm, który zapewne większości z Czytelników pierwszy przyszedłby
| |
| do głowy, gdyż realizuje wprost powszechnie znaną regułę mnożenia macierzy
| |
| "wiersz przez kolumnę". W pamięci ''cache ''L1 mieści się 64KB danych i
| |
| jest ona podzielona na 512 klas odpowiadających kolejnym liniom pamięci. W
| |
| każdej klasie można zmieścić 2 linie pamięci (Duron ma
| |
| ''2-way set associative cache''), a w każdej linia pamięci (i ''cache '''a) składa się z 64
| |
| bajtów, czyli mieści 8 liczb <code>double</code>.
| |
|
| |
| Odwołując się w najgłębszej pętli do kolejnych elementów macierzy <math>\displaystyle A</math> ''oraz'' <math>\displaystyle B</math> powodujemy, że przy odwoływaniu się do <math>\displaystyle B</math>,
| |
| ''cache miss ''następuje praktycznie w każdym kroku. Dzieje się tak dlatego,
| |
| że wymiary naszych macierzy są wielokrotnością 512: odwołując się
| |
| do kolejnych <code>B[k*N+j]</code>, <code>k</code> <nowiki>=</nowiki> <math>\displaystyle 0\ldots N</math>, odwołujemy się do co
| |
| 1024. elementu wektora B, a zatem kolejne odwołania lądują w tej samej sekcji
| |
| ''cache '''a -- która mieści ledwie 2 linie. Skutkiem tego, że w każdym
| |
| obrocie pętli mamy chybienie, jest złudzenie pracowania z komputerem ''bez'' pamięci ''cache ''(a nawet gorzej, bo ''cache miss ''dodatkowo
| |
| kosztuje) czyli jakby z komputerem o szybkości 10 MHz <nowiki>=</nowiki> 100 MHz/10 (bo
| |
| magistrala (''bus '') jest taktowana 100 MHz, a odwołanie do pamięci RAM
| |
| kosztuje mniej więcej 10 cykli magistrali). I rzeczywiście, wyniki zdają się to
| |
| potwierdzać.
| |
|
| |
|
| {{algorytm||| | | {{algorytm||| |
| uuu
| |
| }}
| |
| {{wyniki|||
| |
| uuu
| |
| }}
| |
|
| |
| ====Algorytm ikj====
| |
|
| |
| Różni się on od poprzedniego jedynie
| |
| kolejnością dwóch wewnętrznych pętli:
| |
|
| |
| {{kod|ikj||
| |
| <pre>
| |
|
| |
| /* ikj */
| |
| for (i <nowiki>=</nowiki> 0; i < N; i++)
| |
| for (k <nowiki>=</nowiki> 0; k < N; k++)
| |
| for (j <nowiki>=</nowiki> 0; j < N; j++)
| |
| C[i*N+j] +<nowiki>=</nowiki> A[i*N+k]*B[k*N+j];
| |
| </pre>}}
| |
|
| |
| Okazuje się, że taka prosta zmiana dramatycznie poprawia sytuację!
| |
|
| |
| Tym razem, w odwołaniu do <math>\displaystyle B</math> w wewnętrznej pętli, ''cache miss '' będzie
| |
| następował ośmiokrotnie rzadziej, gdyż odwołując się do ''kolejnych''
| |
| elementów wektora <code>B</code>, znacznie częściej odwołujemy się do danych
| |
| znajdujących się w ''cache '',
| |
| zachowując zasadę ''lokalności w przestrzeni'': ponieważ w linii ''cache '''a
| |
| mieści się osiem ''kolejnych'' elementów wektora <code>B</code>. Stąd
| |
| znaczące przyspieszenie (więcej niż ośmiokrotne --- dlaczego?).
| |
|
| |
| ====Algorytm bikj()====
| |
|
| |
| Algorytm bikj(16) jest prostą wersją algorytmu blokowego operującego w sposób
| |
| "ikj" na blokach macierzy wymiaru <math>\displaystyle 16\times 16</math>:
| |
|
| |
| {{kod|bikj(16)||
| |
| <pre>
| |
|
| |
| /* bikj(16) */
| |
| for (i <nowiki>=</nowiki> 0; i < N; i+<nowiki>=</nowiki>16)
| |
| for (k <nowiki>=</nowiki> 0; k < N; k+<nowiki>=</nowiki>16)
| |
| for (j <nowiki>=</nowiki> 0; j < N; j+<nowiki>=</nowiki>16)
| |
| for (ii <nowiki>=</nowiki> i; ii < i+15; ii++)
| |
| for (kk <nowiki>=</nowiki> k; kk < k+15; kk++)
| |
| for (jj <nowiki>=</nowiki> j; jj < j+15; jj++)
| |
| C[ii*N+jj] +<nowiki>=</nowiki> A[ii*N+kk]*B[kk*N+jj];
| |
|
| |
| </pre>}}
| |
|
| |
| (podobnie działał algorytm bikj(32), tym razem na blokach <math>\displaystyle 32\times 32</math>).
| |
|
| |
| Wadą poprzedniego algorytmu, ikj, było to, że w dwu zewnętrznych pętlach powracał do
| |
| wszystkich <math>\displaystyle N^2</math> wartości <math>\displaystyle C</math> i <math>\displaystyle B</math>, przecząc zasadzie ''lokalności w
| |
| czasie''. Wydzielenie w algorytmie bijk(16) operacji na blokach macierzy ma na celu uniknięcie tego
| |
| problemu: trzy najbardziej zewnętrzne pętle to <math>\displaystyle (1024/16)^3</math> obrotów, czyli
| |
| czterokrotnie mniej niż dwie najbardziej zewnętrzne pętle w poprzednim
| |
| algorytmie. Gdyby udało się zachować liczbę ''cache misses ''na poprzednim poziomie,
| |
| można byłoby liczyć na czterokrotne przyspieszenie. ('''Do sprawdzenia: I tak z grubsza jest:
| |
| teoretycznie wszystkie 3 podmacierze powinny mieścić się w cache'u.''')
| |
|
| |
| ====Algorytmy DGEMM i ATLAS DGEMM====
| |
|
| |
| Algorytm DGEMM to był algorytm mnożenia macierzy z pakietu BLAS --- jest to
| |
| właśnie profesjonalny algorytm blokowy, ale niezoptymalizowany na naszą
| |
| architekturę. Dopiero algorytm DGEMM podrasowany w pakiecie ATLAS dał nam
| |
| sprawność wynoszącą 1.2 Gflopów na sekundę -- czyli praktycznie
| |
| maksimum (''teoretycznie'', z Durona można wycisnąć dwa flopy w cyklu
| |
| zegara, co dawałoby <math>\displaystyle r_{\max}</math> <nowiki>=</nowiki> 2.2 Gflop/s, ale w praktyce jest to mało
| |
| prawdopodobne) tego,
| |
| co da się wycisnąć z tej wersji Durona na liczbach podwójnej precyzji.
| |
|
| |
| ===Macierze w pamięci komputera===
| |
|
| |
| Do implementacji algorytmów numerycznych powszechnie używa się dwóch języków:
| |
| Fortranu i C. Zgodnie z naszą konwencją, skupimy się na programach w C, nie
| |
| możemy jednak przejść obojętnie wobec faktu, że jest bardzo wiele znakomitego
| |
| oprogramowania numerycznego w Fortranie. W Rozdziale [[##sec:FortranC|Uzupelnic: sec:FortranC ]] zajmiemy się
| |
| metodą włączenia podprogramów fortranowskich do programu w C. Zanim jednak to
| |
| uczynimy, musimy zauważyć, że oba języki przechowują macierze w pamięci
| |
| komputera w odmienny sposób, co stanowi źródło potencjalnych poważnych kłopotów
| |
| na styku tych języków. Dlatego w niniejszym rozdziale zechcemy szczegółowo
| |
| przyjrzeć się temu, jak oba te języki przechowują w pamięci macierze i
| |
| opracować sposoby postępowania gwarantujące zgodność programów w obu
| |
| językach.
| |
|
| |
| W Fortranie, elementy macierzy są przechowywane w pamięci kolumnami, tzn. jeśli
| |
| mamy do czynienia z macierzą prostokątną <math>\displaystyle n\times m</math> o elementach <math>\displaystyle a_{ij}</math>,
| |
| <math>\displaystyle i=1\ldots n</math>, <math>\displaystyle j=1\ldots m</math>,
| |
|
| |
| <center><math>\displaystyle
| |
| \begin{pmatrix}
| |
| a_{11} & \cdots & a_{1m}\\
| |
| \vdots & & \vdots\\
| |
| a_{n1} & \cdots & a_{nm}
| |
| \end{pmatrix} .
| |
| </math></center>
| |
|
| |
| to kolejne miejsca w przestrzeni adresowej
| |
| zajmują elementy
| |
| <center><math>\displaystyle a_{11}, a_{21},\ldots, a_{n1}, a_{12}, a_{22}, \ldots, a_{n2},
| |
| \ldots a_{nm}.
| |
| </math></center>
| |
|
| |
| Dla odmiany, C przechowuje w pamięci elementy macierzy wierszami, tzn. kolejno
| |
| <center><math>\displaystyle a_{11}, a_{12},\ldots, a_{1m}, a_{21}, a_{22}, \ldots, a_{2m}, \ldots
| |
| a_{nm}.</math></center>
| |
|
| |
| Co więcej, standard nie gwarantuje, że kolejne wiersze macierzy będą
| |
| przechowywane w przylegających do siebie obszarach pamięci. Bierze się to stąd,
| |
| że w C macierz dwuwymiarowa jest w istocie tablicą wskaźników do kolejnych
| |
| wierszy.
| |
|
| |
| To zaś powoduje kolejne komplikacje. Otóż w procedurach numerycznych bardzo
| |
| często pragniemy dynamicznie przydzielać pamięć na tablice, to znaczy dopiero
| |
| w trakcie działania procedury, gdyż dopiero w trakcie obliczeń procedura
| |
| otrzymuje np. informację o rozmiarze potrzebnej macierzy. Przykładowo, program w C,
| |
| który wykonywałby dynamicznie przydzielałby pamięć na dwuwymiarową tablicę,
| |
| musiałby:
| |
| * przydzielić pamięć na tablicę wskaźników do wierszy
| |
| * każdemu wskaźnikowi do wiersza przydzielić pamięć na pojedynczy wiersz
| |
|
| |
| To jest jeden z licznych powodów, dla których posługując się macierzami w C
| |
| będziemy stosowali pewien prosty ''trick ''.
| |
|
| |
| Otóż przyjmiemy konwencję, że nie będziemy wcale korzystać z macierzy
| |
| dwu- i więcejwymiarowych, a elementy macierzy zapiszemy do jednego odpowiednio
| |
| długiego wektora. W ten sposób wszystkie elementy macierzy zajmą spójny
| |
| obszar pamięci. Przykładowo, macierz wymiaru <math>\displaystyle n\times m</math> będziemy zapisywali do wektora
| |
| o długości <math>\displaystyle n\cdot m</math>.
| |
|
| |
| Mając pełną dowolność wyboru sposobu ułożenia elementów macierzy w wektorze,
| |
| wybierzemy zazwyczaj układ kolumnowy (czyli fortranowski) (niektóre biblioteki w
| |
| C (np. [http://www.fftw.org FFTW]) wymagają jednak układu wierszowego!),
| |
| co ma dwie zalety: po pierwsze, da nam zgodność z Fortranem, a po drugie, może
| |
| potencjalnie uprościć nam optymalizację "książkowych" algorytmów, których
| |
| większość była i często nadal jest pisana z myślą o implementacji w Fortranie.
| |
| Złudzenie korzystania z macierzy dwuwymiarowej da nam zaś makro, lokalizujące
| |
| <math>\displaystyle (i,j)</math>-ty element macierzy w wektorze przechowującym jej elementy. Dodatkowo,
| |
| makro tak skonstruowaliśmy, aby móc indeksować elementy macierzy poczynając od
| |
| 1, czyli <math>\displaystyle a_{ij}</math>, <math>\displaystyle i=1\ldots n</math>, <math>\displaystyle j=1\ldots m</math>.
| |
|
| |
| Poniżej pokazuje przykładowy fragment kodu realizującego opisaną wyżej ideę.
| |
|
| |
| Zwróćmy uwagę na dwa sposoby odwoływania się do elementów macierzy. Za pierwszym
| |
| razem, odwołujemy się do kolejnych elementów wektora <code>matrix</code>, gdyż pętle są
| |
| ustawione tak, by przechodzić przez macierz wzdłuż kolejnych kolumn. Dlatego nie
| |
| jest tu konieczne użycie makra <code>IJ()</code>, a sprytne wykorzystanie
| |
| pointera <code>ptr</code>
| |
| pozwala na zrezygnowanie z operacji mnożenia przy odwoływaniu się do kolejnych
| |
| elementów macierzy.
| |
|
| |
| Za drugim razem chcemy wydrukować naszą macierz na ekranie, musimy więc
| |
| odwoływać się do kolejnych ''wierszy'' macierzy (a więc, z punktu
| |
| wykorzystania cache i hierarchii pamięci, fatalnie! -- na szczęście I/O jest
| |
| znacznie wolniejszy od najwolniejszej nawet pamięci RAM). Tym razem więc nie
| |
| unikniemy wywołania makra <code>IJ()</code> (i obliczania wyrażenia <code>i+j*N</code>) przy
| |
| każdym obrocie wewnętrznej pętli.
| |
|
| |
| Inne zalety takiego podejścia do przechowywania macierzy w C to:
| |
| * łatwy dostęp do takich macierzy z funkcji fortranowskich
| |
| * właściwie opracowane makro <code>IJ()</code> pozwala na ominięcie
| |
| problemu indeksowania elementów macierzy (w C macierze indeksujemy od zera, gdy
| |
| tymczasem we wszelkich "ludzkich" algorytmach (i, dodajmy, np. w Octave i
| |
| MATLABie), elementy macierzy indeksowane są od "1";
| |
| * jawny sposób ułożenia elementów macierzy w pamięci zwiększa przenośność i
| |
| odporność implementowanych procedur
| |
|
| |
| Ceną jaką za to płacimy, jest używanie za każdym razem makra w celu odwołania
| |
| się do konkretnego elementu macierzy. Ponadto, można byłoby się przyczepić do
| |
| niepotrzebnie wielokrotnego wyznaczania tego samego iloczynu <code>j*N</code>, gdy
| |
| odwołujemy się do kolejnych elementów kolumny macierzy. Jest to (niewygórowana,
| |
| moim zdaniem) cena, jaką płacimy za przejrzystość, czytelność, elastyczność i
| |
| "wielojęzyczność" (C/Fortran) naszego programu. Dodajmy, że opisane podejście
| |
| nie jest niczym nowym w praktyce numerycznej i jest stosowane w wielu dobrych
| |
| bibliotekach numerycznych; można tu wymienić np. doskonały skądinąd pakiet
| |
| CVODE (macierz w wektorze plus makra <code>IJ()</code>) czy też pakiet CLAPACK
| |
| (macierz w wektorze), zob.
| |
| \cite{clapack-howto}).
| |
|
| |
| Przypomnijmy przy okazji, że ze względu na konstrukcję ''cache '''a spotykaną
| |
| np. w procesorach Intela i AMD, czasem warto stosować tzw. ''array padding ''
| |
| w przypadku, gdy mamy do czynienia z macierzami o wymiarach będących dużą
| |
| potęgą dwójki, zob. Rozdział [[##sec:cache:example|Uzupelnic: sec:cache:example ]].
| |
|
| |
| ===Włączenie zewnętrznej biblioteki fortranowskiej
| |
| do programu===
| |
|
| |
| Wiele spośród doskonałych bibliotek numerycznych zostało napisanych w Fortranie
| |
| 77 (np. ARPACK, LAPACK, ODEPACK). Tymczasem, nasze programy zdecydowaliśmy się
| |
| (ze względów wymienionych w Rozdziale [[##sec:|Uzupelnic: sec: ]]) pisać w języku C. Na
| |
| szczęście, istnieje prosty sposób wykorzystania gotowych pakietów
| |
| fortranowskich przez zewnętrzne programy w C: wystarczy skorzystać z genialnej
| |
| biblioteki <code>f2c</code> lub jej modyfikacji na użytek kompilatora GCC,
| |
| biblioteki <code>gfortran</code>.
| |
|
| |
| Najczęściej jest tak, że daną bibliotekę (fortranowską) instalujemy na swoim
| |
| komputerze z plików źródłowych (np. ściągniętych z Internetu). Instalacja
| |
| takiej biblioteki, powiedzmy, LAPACK'a, kończy się utworzeniem pliku
| |
| <code>liblapack.a</code>, zawierającego skompilowane wszystkie funkcje tej
| |
| biblioteki.
| |
|
| |
| Z uwagi na względnie powszechną dostępność LAPACKa w pakietach RPM,
| |
| właśnie na przykładzie tych bibliotek omówimy sposób wykorzystania bibliotek
| |
| fortranowskich w
| |
| programie w C.
| |
|
| |
| Napiszemy program, który będzie liczył normę zadanego
| |
| wektora, korzystając z funkcji <code>DNRM2</code> biblioteki BLAS.
| |
|
| |
| Najpierw musimy zorientować się, jak wygląda schemat wywołania takiej funkcji w
| |
| Fortranie. Otóż funkcja wygląda następująco:
| |
|
| |
| {{kod|||
| |
| <pre>
| |
| DOUBLE PRECISION FUNCTION DNRM2 ( N, X, INCX )
| |
| * .. Scalar Arguments ..
| |
| INTEGER INCX, N
| |
| * .. Array Arguments ..
| |
| DOUBLE PRECISION X( * )
| |
| * ..
| |
| *
| |
| * DNRM2 returns the euclidean norm of a vector via the function
| |
| * name, so that
| |
| *
| |
| * DNRM2 :<nowiki>=</nowiki> sqrt( x'*x )
| |
| *
| |
| i tak dalej...
| |
| </pre>}}
| |
|
| |
| Tak więc nasza funkcja obliczająca normę wektora ma trzy argumenty: <code>N</code> --
| |
| długość wektora (<code>INTEGER</code>), <code>X</code> -- wektor, którego długość chcemy
| |
| obliczyć (tablica liczb <code>DOUBLE PRECISION</code>) oraz tajemniczy dodatkowy parametr
| |
| <code>INCX</code> typu <code>INTEGER</code> -- jest to wartość skoku, określająca co który
| |
| element wektora uwzględnić w obliczaniu normy: aby policzyć normę całego
| |
| wektora, bierzemy <code>INCX</code> równe 1. Używając zapisu Octave, <code>DNRM2</code>
| |
| oblicza po prostu
| |
|
| |
| {{kod|||
| |
| <pre>
| |
| norm( X(1:INCX:N) )
| |
| </pre>}}
| |
|
| |
| Kod obiektowy tej funkcji znajduje się już w bibliotece BLAS, zawartej w pliku
| |
| <code>libblas.a</code>. Chcielibyśmy wykorzystać tę funkcję w programie w C, a jak
| |
| wiadomo, każda funkcja w C powinna mieć swój prototyp. Jaki powinien być ''prototyp'' tej funkcji?
| |
|
| |
| Przede wszystkim, zacznijmy od nazwy. W przypadku kompilatora
| |
| <code>gcc</code>/<code>gfortran</code>, nazwą funkcji do wykorzystania w C będzie
| |
| <code>dnrm2_</code> (tak! małymi literami i z przyrostkiem "<code>_</code>").
| |
|
| |
| Jeśli zaś chodzi o argumenty, to zapewne co do drugiego nie będziemy mieli
| |
| wątpliwości: jako wektor <code>X</code> przekażemy -- naturalnie -- ''wskaźnik'' do
| |
| tablicy <code>X</code> (typu <code>double</code>), czyli po prostu: jej nazwę. Co z
| |
| pozostałymi argumentami? Okazuje się, że reguła jest niezmienna:
| |
|
| |
| <blockquote> Każdy argument funkcji fortranowskiej zastępujemy ''wskaźnikiem'' do odpowiedniego typu:
| |
|
| |
| {| border=1
| |
| |+ <span style="font-variant:small-caps">Uzupelnij tytul</span>
| |
| |-
| |
| |
| |
| Fortran 77 || C
| |
| |-
| |
| |
| |
| INTEGER || int
| |
| |-
| |
| | REAL || float
| |
| |-
| |
| | DOUBLE PRECISION || double
| |
| |-
| |
| | COMPLEX || struct { float Re, Im; }
| |
| |-
| |
| | DOUBLE COMPLEX || struct { double Re, Im; }
| |
| |-
| |
| | CHARACTER || char
| |
| |-
| |
| |
| |
|
| |
| |}
| |
|
| |
| </blockquote>
| |
|
| |
| <blockquote> Wszystkim argumentom macierzowym danego typu w Fortranie
| |
| (reprezentującym macierze jedno-, dwu-, i więcejwymiarowe) przypisujemy w C
| |
| (pojedynczy) wskaźnik do tego typu (o czym w będzie mowa w następnym
| |
| przykładzie). </blockquote>
| |
|
| |
| A więc pierwszym i trzecim argumentem funkcji <code>dnrm2_</code>
| |
| będą wskaźniki do <code>int</code>. Ponieważ
| |
| funkcja <code>DNRM2</code> zwraca w wyniku liczbę podwójnej precyzji, to ostatecznie
| |
| prototyp naszej funkcji w C będzie następujący:
| |
|
| |
| {{kod|||
| |
| <pre>
| |
| double dnrm2_(int* N, double* X, int* INCX);
| |
| </pre>}}
| |
|
| |
| No to wykorzystajmy naszą funkcję:
| |
|
| |
| {{kod|Wykorzystanie funkcji DNRM2 fortranowskiej biblioteki BLAS w programie w C||
| |
| <pre>
| |
| #include <stdio.h>
| |
| double dnrm2_(int*,double*,int*);
| |
|
| |
| int main(void)
| |
| {
| |
| int n, incx<nowiki>=</nowiki>1;
| |
| double x[3]<nowiki>=</nowiki> {0,1,2};
| |
|
| |
| n <nowiki>=</nowiki> 3;
| |
| printf("Norma podanego wektora:
| |
| return(0);
| |
| }
| |
| </pre>}}
| |
|
| |
| Zwróćmy uwagę na sposób kompilacji tego programu:
| |
|
| |
| {{kod|||
| |
| <pre> | | <pre> |
| gcc -o testblas testblas.c -lblas -lgfortran -lm
| | <math>\displaystyle x_N^*\, =\, c_N / u_{N,N}</math>; |
| | for (i <nowiki>=</nowiki> N-1; i ><nowiki>=</nowiki> 1; i--) |
| | <math>\displaystyle x_i^*\,:=\,\left( c_i\,-\, \sum_{j=i+1}^N |
| | u_{i,j}x_j^*\right)/u_{i,i}</math>; |
| </pre>}} | | </pre>}} |
|
| |
|
| oprócz biblioteki BLAS, co
| | (Algorytm ten jest wykonalny, ponieważ nieosobliwość macierzy |
| naturalne, musimy dołączyć jeszcze bibliotekę matematyczną (może być
| | implikuje, że <math>\displaystyle u_{i,i}\ne 0</math>, <math>\displaystyle \forall i</math>.) Podobnie, układ |
| wykorzystywana przez BLAS!) oraz, co bardzo ważne, specjalną bibliotekę:
| | <math>\displaystyle L x= c</math> rozwiązujemy algorytmem: |
| <code>gfortran</code>, umożliwiającą koegzystencję Fortranu i | |
| C.
| |
|
| |
|
| ====Funkcja fortranowska z argumentem macierzowym====
| | {{algorytm|Gaussa|| |
| | |
| Należy szczególną uwagę zwrócić na argumenty macierzowe funkcji w Fortranie,
| |
| gdyż bardzo łatwo tu o pomyłkę, którą potem trudno wychwycić. Aby niepotrzebnie
| |
| nie komplikować przykładu subtelnościami funkcji BLAS, rozważmy kod źródłowy
| |
| prostej funkcji w Fortranie 77, która po prostu wypełnia liczbami pewną macierz
| |
| wymiaru <math>\displaystyle M\times N</math>:
| |
| | |
| {{kod|Funkcja w Fortranie 77, wypełniająca macierz <math>\displaystyle M\times N</math>|| | |
| <pre> | | <pre> |
|
| |
|
| SUBROUTINE FILLMATRIX ( M, N, MATRIX )
| | <math>\displaystyle x_1 = c_1</math>; |
| INTEGER M,N
| | for (i<nowiki>=</nowiki>2; i <<nowiki>=</nowiki> N; i++) |
| DOUBLE PRECISION MATRIX(M, N)
| | <math>\displaystyle x_i = c_i\,-\,\sum_{j=1}^{i-1} l_{i,j} x_j^*</math>; |
|
| |
| DO 10 I<nowiki>=</nowiki>1,M
| |
| DO 20 J<nowiki>=</nowiki>1,N
| |
| MATRIX(I,J) <nowiki>=</nowiki> I+2*J
| |
| 20 CONTINUE
| |
| 10 CONTINUE
| |
| END
| |
| </pre>}} | | </pre>}} |
|
| |
|
| Nawet osoby nie znające Fortranu domyślą się, że wynikiem działania naszej
| | kod w c |
| funkcji, np. dla <math>\displaystyle M=2</math>, <math>\displaystyle N=5</math>, będzie macierz
| |
| | |
| <center><math>\displaystyle
| |
| \lstF{MATRIX} =
| |
| \begin{pmatrix}
| |
| 3 & 5 & 7 & 9 & 11 \\
| |
| 4 & 6 & 8 & 10 & 12
| |
| \end{pmatrix}
| |
| </math></center>
| |
| | |
| Naturalnie, możemy wywołać ją wprost z programu w C przy użyciu poprzednio
| |
| poznanej techniki i następującego kodu (tym razem prototyp funkcji
| |
| <code>fillmatrix_</code> umieszczamy w osobnym pliku nagłówkowym <code>ffortran.h</code>,
| |
| gdzie mamy zamiar kolekcjonować prototypy w C lokalnie wykorzystywanych funkcji
| |
| fortranowskich):
| |
|
| |
|
| {{kod|Wykorzystanie funkcji fortranowskiej
| | <div style="background-color:red"><pre> |
| operującej na macierzy. Zwróćmy uwagę na sposób użycia argumentu
| |
| macierzowego||
| |
| <pre> | |
| #include <stdio.h> | | #include <stdio.h> |
| #include <stdlib.h>
| | float x; |
| int fillmatrix_(int *, int *, double *);
| | main() |
| | |
| int main()
| |
| {
| |
| int MM, NN, i, j;
| |
| double *A;
| |
|
| |
| MM <nowiki>=</nowiki> 2; NN <nowiki>=</nowiki> 5;
| |
|
| |
| A <nowiki>=</nowiki> (double *)malloc(MM*NN*sizeof(double));
| |
| | |
| fillmatrix_( &MM, &NN, A );
| |
| | |
| printf("\nKolejne elementy wektora A:\n\n");
| |
| for ( i <nowiki>=</nowiki> 0; i < NN*MM ; i++ ){
| |
| printf("}
| |
| | |
| printf("\nWektor A zinterpretowany jako macierz:\n\n");
| |
| for ( j <nowiki>=</nowiki> 0 ; j < MM ; j++ )
| |
| { | | { |
| for ( i <nowiki>=</nowiki> 0; i < NN ; i++ )
| |
| printf("printf("\n");
| |
| }
| |
|
| |
| free( A );
| |
|
| |
| return(0);
| |
| }
| |
| </pre>}}
| |
|
| |
| Zauważmy, że mimo iż funkcja fortranowska odwołuje się do macierzy ''dwuwymiarowej'', jako argument jej wywołania w C przekazujemy ''tablicę
| |
| jednowymiarową'' odpowiedniej wielkości.
| |
|
| |
| ===BLAS, LAPACK i ATLAS===
| |
|
| |
| W zadaniach dotyczących macierzy gęstych, warto skorzystać z klasycznego tandemu
| |
| bibliotek: BLAS (''Basic Linear Algebra Subprograms '') \cite{BLAS-home-page}
| |
| oraz LAPACK (''Linear Algebra PACKage '') \cite{LAPACK-home-page}. Dla macierzy
| |
| rozrzedzonych (zawierających wiele elementów zerowych) warto zastosować bardziej
| |
| wyspecjalizowane biblioteki. Aby wykorzystać do maksimum moc oferowaną przez te
| |
| biblioteki (w połączeniu z naszym komputerem) warto skorzystać z optymalizowanej
| |
| wersji BLASów i LAPACKa, czyli z ATLASa, \cite{ATLAS-home-page}. Istnieje inna
| |
| wersja optymalizowanych BLASów, tzw. Goto BLAS. Niektóre procedury ATLASa są
| |
| istotnie szybsze od standardowych BLASów, dając, przykładowo dla zadania
| |
| mnożenia dwóch macierzy (na komputerze z procesorem Pentium 4 i
| |
| dostatecznie dużych macierzy), '''ponaddziesięciokrotne przyspieszenie''' na
| |
| zmiennych typu <code>float</code> i <code>double</code> i około pięciokrotne na zmiennych
| |
| typu <code>complex </code> i <code>double complex</code>.
| |
|
| |
| Aby osiągnąć największe przyspieszenie, bibliotekę ATLAS należy skompilować
| |
| samemu na własnej (''nieobciążonej'' w trakcie kompilacji!) architekturze. W plikach
| |
| <code>Makefile</code> ATLASa brak jednak opcji instalacji bibliotek w standardowych
| |
| miejscach --- trzeba zrobić to samemu.
| |
|
| |
| BLAS i LAPACK są często wykorzystywane przez inne biblioteki numeryczne,
| |
| na nich opierają się również funkcje macierzowe w Octave i MATLABie.
| |
| Optymalizowane biblioteki BLAS i LAPACK dla swoich architektur oferują
| |
| producenci procesorów Intel (biblioteka MKL) oraz AMD (biblioteka ACML)
| |
|
| |
| BLAS \cite{BLAS-home-page} jest kolekcją procedur służących manipulacji podstawowymi obiektami
| |
| algebry liniowej: skalarami, wektorami i macierzami. Obsługiwane są obiekty
| |
| rzeczywiste (w pojedynczej albo podwójnej precyzji) oraz zespolone (także w
| |
| dwóch precyzjach). W BLAS wyróżnia się 3 poziomy abstrakcji algorytmów:
| |
| * BLAS Level 1 -- działania typu wektor-wektor, np. operacja AXPY, czyli
| |
| uogólnione
| |
| dodawanie wektorów
| |
| <center><math>\displaystyle
| |
| y \leftarrow \alpha x + y,
| |
| </math></center>
| |
|
| |
| albo obliczanie normy wektora. BLAS Level 1 ma za zadanie porządkować kod;
| |
| * BLAS Level 2 -- działania typu macierz--wektor, np. mnożenie macierzy
| |
| przez wektor
| |
| <center><math>\displaystyle
| |
| y \leftarrow \alpha A x + y
| |
| </math></center>
| |
|
| |
| Zapis algorytmów z użyciem BLAS Level 2 umożliwia potencjalnie przyspieszenie
| |
| programu, m.in. ze względu na to, że zoptymalizowane procedury BLAS Level 2 mogą np.
| |
| wykorzystywać instrukcje wektorowe nowoczesnych procesorów, zob.
| |
| Rozdział [[##sec:architektura|Uzupelnic: sec:architektura ]];
| |
| * BLAS Level 3 -- operacje typu macierz--macierz, np. mnożenie dwóch
| |
| macierzy:
| |
| <center><math>\displaystyle
| |
| C \leftarrow \alpha A\cdot B + C
| |
| </math></center>
| |
|
| |
| W związku z tym, że operacje macierzowe wymagają wykonania <math>\displaystyle O(N^3)</math> działań
| |
| arytmetycznych przy
| |
| <math>\displaystyle O(N^2)</math> danych (gdzie <math>\displaystyle N</math> jest wymiarem macierzy), wykorzystanie
| |
| zoptymalizowanych procedur BLAS Level 3 może znacząco przyspieszyć wykonywanie
| |
| obliczeń na maszynach z pamięcią hierarchiczną (zob. Rozdział [[##sec:cache|Uzupelnic: sec:cache ]]).
| |
|
| |
| Podsumowując: przyjmijmy, że dysponujemy dobrze zoptymalizowaną biblioteką BLAS.
| |
| Wówczas dany algorytm algebry liniowej najlepiej zapisać przy użyciu procedur BLAS Level
| |
| 3, naturalnie, pod warunkiem, że w ogóle daje się to zrobić; typową strategią w
| |
| takim wypadku jest tworzenie algorytmów blokowych, operujących na ''blokach''
| |
| macierzy, zamiast na jej pojedynczych elementach.
| |
|
| |
| Większość użytkowników BLAS nie będzie jednak miała potrzeby pisania własnych
| |
| algorytmów blokowych, gdyż funkcje rozwiązujące podstawowe zadania numerycznej
| |
| algebry liniowej: rozwiązywanie układów równań (także nad- i niedookreślonych)
| |
| oraz zadania własnego, znajdują się w doskonałym pakiecie LAPACK
| |
| \cite{LAPACK-home-page}, który
| |
| intensywnie i skutecznie wykorzystuje podprogramy BLAS.
| |
|
| |
| Nazwy procedur BLASów i
| |
| LAPACKa są cokolwiek enigmatyczne na pierwszy rzut oka, ale w istocie bardzo
| |
| łatwo je odgadywać. Każda nazwa składa się z kilku części, najczęściej jest
| |
| postaci <code>PRRFF</code>, gdzie
| |
|
| |
| ;
| |
| : <code>P</code> oznacza precyzję i może przyjmować
| |
| wartości: S,D,C,Z, odpowiadające kolejno pojedynczej i podwójnej precyzji w
| |
| dziedzinie
| |
| rzeczywistej i pojedynczej i podwójnej precyzji w dziedzinie zespolonej;
| |
|
| |
| ;
| |
| : <code>RR</code> oznacza rodzaj zadania, np. GE oznacza ''GEneral '', czyli zadanie ogólne
| |
| (praktycznie bez założeń), a SY oznacza ''SYmmetric '', czyli zadanie symetryczne;
| |
|
| |
| ;
| |
| : <code>FF</code> wreszcie określa samo zadanie, np. SV oznacza
| |
| ''SolVe ''(w domyśle: układ równań), MV --- ''Matrix-Vector ''(w domyśle: mnożenie),
| |
| EV --- ''EigenValues '', czyli wartości własne, itp. Są też warianty
| |
| trzyliterowe, np. TRF (''TRiangular Factorization '') i TRS (''TRiangular
| |
| Solve ''--- w domyśle, przy użyciu wcześniej wyznaczonej faktoryzacji)
| |
|
| |
| Jeśli jednak ''nie możemy zgadnąć'', jaka jest nazwa procedury BLAS/LAPACKa,
| |
| która byłaby nam potrzebna,
| |
| najlepiej przejrzeć (przeszukać) strony internetowe tych pakietów w serwisie
| |
| Netlib.
| |
|
| |
| Zestawienie najczęściej wykorzystywanych procedur BLAS i LAPACKa przedstawiamy
| |
| poniżej. Każda z tych procedur ma swój wariant "ekspercki", np. dla
| |
| rozwiązywania układów równań metodą eliminacji Gaussa można skorzystać z
| |
| osobnych procedur generujących rozkład LU oraz z innych, rozwiązujących układy
| |
| trójkątne.
| |
|
| |
| {| border=1
| |
| |+ <span style="font-variant:small-caps">Uzupelnij tytul</span>
| |
| |-
| |
| |
| |
|
| |
| Zadanie algebry liniowej || Nazwa procedury BLAS/LAPACK
| |
| |-
| |
| |
| |
|
| |
| mnożenie wektora przez macierz || DGEMV
| |
| |-
| |
| | mnożenie macierzy przez macierz || DGEMM
| |
| |-
| |
| |
| |
| rozwiązywanie układu równań || DGESV
| |
| |-
| |
| | rozkład LU (w miejscu) || DGETRF
| |
| |-
| |
| | rozwiązywanie układu z rozkładem uzyskanym z DGETRF || DGETRS
| |
| |-
| |
| |
| |
| rozwiązywanie układu z macierzą symetryczną || DSYSV
| |
| |-
| |
| | rozkład LDL<math>\displaystyle ^T</math> macierzy symetrycznej (w miejscu) || DSYTRF
| |
| |-
| |
| | rozwiązywanie układu z rozkładem uzyskanym z DSYTRF || DSYTRS
| |
| |-
| |
| |
| |
| rozwiązywanie układu z macierzą pasmową || DGBSV
| |
| |-
| |
| | rozkład LU macierzy pasmowej (w miejscu) || DGBTRF
| |
| |-
| |
| | rozwiązywanie układu z rozkładem uzyskanym z DGBTRF || DGBTRS
| |
| |-
| |
| |
| |
| zagadnienie własne || DGESV
| |
| |-
| |
| |
| |
|
| |
| |}
| |
|
| |
| ====Mnożenie macierz-wektor w BLAS====
| |
|
| |
| Zacznijmy od prostej procedury BLAS Level 2, jaką jest mnożenie macierzy przez
| |
| wektor. Wykorzystamy tzw. wzorcową implementację BLASów (niezoptymalizowaną)
| |
| dostarczaną z dystrybucją np. Red Hat Linux. Jest to biblioteka funkcji
| |
| fortranowskich.
| |
|
| |
| Naszym zadaniem jest wykonanie operacji
| |
| <center><math>\displaystyle
| |
| y \leftarrow \alpha A x + y,
| |
| </math></center>
| |
|
| |
| gdzie <math>\displaystyle A</math> jest zadaną macierzą <math>\displaystyle N\times M</math>, natomiast <math>\displaystyle y</math> jest wektorem o <math>\displaystyle M</math>
| |
| współrzędnych.
| |
|
| |
| To zadanie realizuje procedura BLASów o nazwie
| |
| <code>DGEMV</code>. W rzeczywistości, ta procedura wykonuje ogólniejsze zadanie
| |
| wyznaczania wektora
| |
|
| |
| <center><math>\displaystyle
| |
| y \leftarrow \alpha B x + \beta y,
| |
| </math></center>
| |
|
| |
| przy czym macierz <math>\displaystyle B</math> może być równa albo <math>\displaystyle A</math>, albo <math>\displaystyle A^T</math> (jednak za każdym
| |
| razem argumentem macierzowym, jaki przekazujemy <code>DGEMV</code>, jest wyjściowa
| |
| macierz <math>\displaystyle A</math>).
| |
|
| |
| Jak wiemy, że jest możliwe bezpośrednie wykorzystanie
| |
| biblioteki fortranowskiej w programie w C, jednak musimy pamiętać, iż macierze z
| |
| jakich ona skorzysta muszą być ułożone ''kolumnami'' w jednolitym bloku
| |
| pamięci.
| |
|
| |
| Bazując na opisie procedury <code>DGEMV</code> ze
| |
| strony \pageref{opis:dgemv}, w programie w C powinniśmy
| |
| napisać prototyp tej funkcji następująco:
| |
|
| |
| {{kod|||
| |
| <pre>
| |
| int dgemv_( char* TRANS,
| |
| int* M,
| |
| int* N,
| |
| double* ALPHA,
| |
| double* A,
| |
| int* LDA,
| |
| double* X,
| |
| int* INCX,
| |
| double* BETA,
| |
| double* Y,
| |
| int* INCY );
| |
| </pre>}}
| |
|
| |
| Dla własnej wygody, a także dla przyszłego wykorzystania, umieścimy ten
| |
| prototyp, razem z innymi przydatnymi drobiazgami (m.in. makro <code>IJ</code>
| |
| dające wygodny dostęp do macierzy niezależny od jej wewnętrznej reprezentacji, a
| |
| także zmienne całkowite
| |
| <code>static int BLASONE <nowiki>=</nowiki> 1, BLASMONE <nowiki>=</nowiki> -1;</code>), w pliku
| |
| nagłówkowym <code>blaslapack.h</code>.
| |
|
| |
| Dalej już jest łatwo: oto pełny kod programu realizującego operację mnożenia macierzy przez wektor
| |
| przy użyciu procedury BLAS <code>DGEMV</code>:
| |
|
| |
| {{kod|||
| |
| <pre>
| |
| #include <stdio.h>
| |
| #include "blaslapack.h"
| |
|
| |
| double* mmread(char *filename, int* N, int* M );
| |
|
| |
| int main()
| |
| {
| |
| int N, M, i, j;
| |
| double *A, *x, *y;
| |
|
| |
| /* wczytujemy macierz z pliku w formacie MatrixMarket */
| |
| /* macierz jest reprezentowana w formacie kolumnowym */
| |
| A <nowiki>=</nowiki> mmread("mbeacxc.mtx", &N, &M );
| |
|
| |
| x <nowiki>=</nowiki> (double *)malloc(N*sizeof(double));
| |
| y <nowiki>=</nowiki> (double *)malloc(M*sizeof(double));
| |
| for (i <nowiki>=</nowiki> 1; i <<nowiki>=</nowiki> N; i++)
| |
| x[IJ(i,1,N)] <nowiki>=</nowiki> (double)i;
| |
|
| |
| /* obliczamy y <nowiki>=</nowiki> 5*A*x, korzystając z procedury BLAS L2: DGEMV */
| |
|
| |
| {
| |
| char TRANS <nowiki>=</nowiki> 'N'; double ALPHA <nowiki>=</nowiki> 5.0, BETA <nowiki>=</nowiki> 0.0;
| |
|
| |
| dgemv_(&TRANS, &N, &M, &ALPHA, A, &N, x, &BLASONE,
| |
| &BETA, y, &BLASONE );
| |
|
| |
| } | | } |
| | | |
| /* wydruk wyniku */
| | </pre></div> |
| for (i <nowiki>=</nowiki> 1; i <<nowiki>=</nowiki> M; i++)
| | |
| printf("
| | wyniki |
| return(0);
| |
| }
| |
| </pre>}} | |
| | |
| Zwróćmy uwagę na wykorzystanie "stałej" <code>BLASONE</code>, równej 1,
| |
| predefiniowanej w pliku <code>blaslapack.h</code>. Nasz program kompilujemy
| |
| standardowo, pamiętając o dołączeniu na etapie linkowania używanych przez nas
| |
| bibliotek:
| |
| | |
| {{kod|||
| |
| <pre>
| |
| gcc -o testblas testblas.c -llapack -lblas -lgfortran -lm
| |
| </pre>}}
| |
|
| |
|
| --- dokładnie ''w tej właśnie kolejności'' (LAPACK oczywiście w tym momencie
| | \beginoutux |
| dołączamy na wyrost: nasz program nie korzysta z żadnej funkcji LAPACKa, wobec
| | to są wyniki |
| tego opcja <code>-llapack</code> zostanie zignorowana).
| | \endoutux |
|
| |
|
| Pamiętamy oczywiście, że standardowe BLASy i LAPACK nie są zoptymalizowane w
| | A jeśli masz komendę <code>DGESV <nowiki>=</nowiki> 5</code> to co? |
| stopniu pozwalającym na (prawie) maksymalne wykorzystanie możliwości sprzętu.
| |
| Dla osiągnięcia maksymalnej efektywności kodu, trzeba skorzystać z
| |
| optymalizowanych BLAS, które obecnie są dostępne nawet w kilku wariantach na
| |
| architektury x86.
| |