MN06: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przykry (dyskusja | edycje)
mNie podano opisu zmian
Przykry (dyskusja | edycje)
mNie podano opisu zmian
Linia 1: Linia 1:


==Rozwiązywanie wielkich układów równań liniowych==
=Pamięć hierarchiczna komputerów a algorytmy numeryczne=


Wraz z coraz większymi modelami pojawiającymi się w praktyce obliczeniowej,
W poprzednim wykładzie sprawdziliśmy, jaki jest koszt obliczeniowy algorytmu
coraz częściej zachodzi potrzeba rozwiązywania zadań algebry liniowej, w której
eliminacji Gaussa. Jednak w obecnym świecie istotna jest nie tylko liczba
macierze są co prawda wielkiego wymiaru, ale najczęściej ''rozrzedzone'', to
operacji arytmetycznych, ale także koszt pobrania danych z pamięci. Za chwilę
znaczy jest w nich bardzo dużo zer. Bardzo często zdarza się, że macierz
zobaczymy, że poprzez <strong>reorganizację kolejności obliczeń</strong> w algorytmie
wymiaru <math>\displaystyle N</math> ma tylko <math>\displaystyle O(N)</math> niezerowych elementów. Wykorzytanie tej specyficznej
eliminacji Gaussa, możemy dostać algorytm (tzw. algorytm blokowy), którego
własności macierzy nie tylko prowadzi do algorytmów istotnie szybszych od ich
implementacja, choć znacznie mniej czytelna niż powyżej, będzie <strong>znacznie</strong>
analogów dla macierzy gęstych (to znaczy takich, które (w założeniu) mają <math>\displaystyle N^2</math>
szybsza!
elementów), ale wręcz są jedynym sposobem na to, by niektóre zadania w ogóle
stały się rozwiązywalne przy obecnym stanie techniki obliczeniowej!


Jednym ze szczególnie ważnych źródeł układów równań z macierzami rozrzedzonymi
Bez dostatecznie szybkiej pamięci, procesor -- zamiast liczyć -- będzie
są np. '''równania różniczkowe cząstkowe''' (a więc np. modele pogody, naprężeń w
większość czasu czekał na dane, a jego efektywność drastycznie spadnie. Z
konstrukcji samochodu, przenikania kosmetyków do głębszych warstw skóry, itp.).
niewielką przesadą można powiedzieć, że


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<blockquote  style="background-color:#fefeee">   W optymalizacji szybkości działania programu numerycznego,
<span  style="font-variant:small-caps;">Przykład: Macierz z kolekcji Boeinga</span>  
obecnie cała walka idzie o to, by procesor przez cały czas <strong>miał co liczyć</strong>.
<div class="solution">
</blockquote>  


Macierz sztywności dla modelu silnika lotniczego, wygenerowana swego czasu w
Szczególnie jest to widoczne w algorytmach, które wykonują bardzo dużo operacji
zakładach Boeinga. Pochodzi z
na dużej liczbie
[http://www.cise.ufl.edu/research/sparse/matrices/Boeing  kolekcji Tima
danych --- a tak jest m.in. w algorytmach algebry liniowej takich jak mnożenie
Davisa] (autora bardzo dobrego solvera równań liniowych z macierzami rzadkimi).
dwóch macierzy, czy rozwiązywanie układów równań liniowych: te algorytmy
Jest to mała macierz, wymiaru 8032 (w kolekcji spotkasz równania z milionem i
najczęściej operują na <math>\displaystyle O(N^2)</math> danych i wykonują aż <math>\displaystyle O(N^3)</math> działań.
więcej
niewiadomych).


[[Image:MNsparseA.png|thumb|400px||Struktura niezerowych elementów macierzy bcsstk38.]]
Ponieważ szybka pamięć komputerowa jest jednocześnie bardzo droga,
konstruktorzy komputerów osobistych stoją przed wykluczającymi się celami: z
jednej strony powinni zapewnić użytkownikowi jak najwięcej pamięci, z drugiej
zaś -- chcieliby zapewnić użytkownikowi jak najszybszą pamięć. Sprzeczność tych
dążeń ujawnia się, gdy dołączyć do nich trzecie, ale zasadnicze wymaganie:
całość ma być w rozsądnej cenie... Ostatecznie więc, z biegiem lat dramatycznie
pogłębia się przepaść pomiędzy  prędkością (podwajającą się, zgodnie z
heurystycznym prawem Moore'a, co półtora roku) procesora, a prędkością pamięci
RAM, do której procesor musi się odwoływać.


</div></div>
Powszechnie przyjętym sposobem pogodzenia tych sprzeczności jest pamięć
hierarchiczna.  Koncept polega na tym, że część pamięci, która najczęściej komunikuje się z  procesorem,
jest bardzo szybka (lecz jest jej relatywnie mało), natomiast pozostała pamięć
jest wolniejsza, za to może jej być bardzo dużo.


Modele wielostanowych systemów kolejkowych (np. routera obsługującego wiele komputerów) także
W praktycznej realizacji, zamiast dwóch poziomów pamięci (mała-szybka i
prowadzą do gigantycznych układów równań z macierzami rozrzedzonymi o
duża-wolna), w komputerze występuje wiele poziomów:
specyficznej strukturze.
* rejestry procesora
* ''cache ''(pamięć podręczna) procesora
* ''cache ''drugiego poziomu (ostatnio także wbudowywana do procesora)
* pamięć operacyjna (RAM): główna pamięć komputera
* pamięć zewnętrzna (np. twardy dysk)
* pamięć masowa (CD-ROM, taśma streamera, itp.)
Efektywność działania komputera, zwłaszcza w wielkoskalowych obliczeniach
numerycznych, bardzo istotnie zależy od skutecznego wykorzystania hierarchii
pamięci tak, by jak największa część operacji była wykonywana na zmiennych
znajdujących się w danej chwili w jak najszybszej pamięci procesora.


Z reguły zadania liniowe wielkiego wymiaru będą miały strukturę macierzy
Kluczem do skutecznego wykorzystania hierarchii pamięci jest zasada
rozrzedzonej, gdyż najczęściej związki pomiędzy niewiadomymi w równaniu nie
lokalności w czasie i w przestrzeni:
dotyczą wszystkich, tylko wybranej grupy.


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<blockquote  style="background-color:#fefeee">
<span  style="font-variant:small-caps;">Przykład: Macierz MBEACXC</span>  
* <strong>Lokalność w czasie:</strong> Używać danego fragmentu pamięci intensywnie, ale rzadko.
<div class="solution">
* <strong>Lokalność w przestrzeni (adresowej):</strong> W danej chwili, odnosić
się do adresów pamięci leżących blisko siebie.
</blockquote>  


Dane pochodzą z serwisu
Zachowanie zasady lokalności w czasie jest bardzo ważne dla efektywnego
[http://math.nist.gov/MatrixMarket  MatrixMarket]. Jest to niezbyt duża macierz, wymiaru
wykorzystania cache'a -- ze względu na małe rozmiary tej pamięci, wymiana
<math>\displaystyle 496 \times 496</math>, niesymetryczna, o elementach rzeczywistych. Źródłem tej
zmiennych może być tam intensywna. Zasada lokalności w przestrzeni też jest
macierzy jest pewien model ekonomiczny, dotyczący gospodarki amerykańskiej w
ważna, przy czym nie tylko ze względu na efektywne wykorzystanie cache'a, ale
1972r. Struktura niezerowych elementów tej macierzy jest przedstawiona na
także dla efektywnego wykorzystania pamięci
Rysunku poniżej.
wirtualnej.


[[Image:MNmbeacxc|thumb|400px||Struktura niezerowych elementów macierzy MBEACXC]]
==Jak napisać kod źle wykorzystujący pamięć podręczną?==


Tylko pozornie macierz ta wydaje się dość gęsta, w rzeczywistości jej współczynnik wypełnienia
Choć programista nie ma bezpośredniego wpływu na opisane powyżej działania
wynosi około 20 procent.
systemu operacyjnego i ''hardware'''u (zarządzających, odpowiednio, pamięcią
</div></div>
wirtualną i ''cache''), to przez właściwe projektowanie algorytmów -- a
zwłaszcza: ich <strong>właściwą</strong> implementację -- może spowodować, że jego
programy nie będą zmuszały komputera do nieracjonalnych zachowań. Jak się
okazuje, całkiem łatwo nieświadomie tworzyć programy przeczące zasadom
efektywnego wykorzystania hierarchii pamięci. Na potwierdzenie zacytujmy klasyczny już przykład mnożenia dwóch macierzy.


===Formaty macierzy rzadkich===
W programie wykonujemy operację mnożenia dwóch macierzy <math>\displaystyle 1024\times 1024</math> przy
użyciu kilku <strong>matematycznie równoważnych</strong> algorytmów (nazwaliśmy je umownie
ijk, ikj, bikj(<math>\displaystyle \cdot</math>) --- nazwy pochodzą od sposobu organizacji pętli, zob.
poniżej), zaimplementowanych w programie C, wykorzystującym technikę
pozwalającą przechowywać macierze w pamięci komputera kolumnowo. Dla porównania zmierzyliśmy czas
wykonania tej samej operacji przy użyciu wyspecjalizowanych bibliotek z
pakietów BLAS (algorytm DGEMM) i ATLAS (algorytm ATLAS DGEMM). Oto jakie wyniki uzyskaliśmy dla obliczeń w
arytmetyce podwójnej precyzji <code>double</code> na maszynie z procesorem AMD Duron
i zegarem 1.1 GHz:


Zacznijmy od sposobu przechowywania macierzy rozrzedzonych. Naturalnie, nie ma
{| border=1
sensu przechowywać wszystkich ''zerowych'' jej elementów: wystarczy
|+ <span style="font-variant:small-caps"> </span>
ograniczyć się do zachowania tych niezerowych! W ten sposób zmniejszamy zarówno
|-
wymagania pamięciowe, jak i liczbę operacji zmiennoprzecinkowych potrzebnych do
|
prowadzenia działań na macierzy (np. w przypadku mnożenia macierzy przez
Algorytm  ||  ijk  ||  ikj  ||  bikj(16)  ||  bikj(32)  ||  DGEMM  ||  ATLAS DGEMM
wektor, nie będziemy mnożyć przez zera!).  
|-
|
Czas (s)  ||  320.49  ||  24.28  ||  8.68  ||  30.45  ||  25.72  ||  2.58
|-
|
Mflop/s  ||  10.06  ||  132.67  ||  371.11  ||  105.79  ||  125.24  ||  1248.53
|-
|


====Format współrzędnych (AIJ)====
|}


Do zapamiętania macierzy
Jak widać, różnice pomiędzy --- podkreślmy, matematycznie równoważnymi ---
<math>\displaystyle A</math> wymiaru <math>\displaystyle N\times M</math> o <math>\displaystyle NNZ</math> niezerowych elementów, wykorzystujemy trzy wektory: <code>I</code>,
algorytmami są bardzo znaczące; w szczególności  algorytm ijk wydaje się nie do
<code>J</code> --- oba typu <code>int</code> --- oraz <code>V</code>, typu <code>double</code>,
przyjęcia! Powodem różnic musi być, ponieważ liczba wykonanych operacji
wszystkie o długości <math>\displaystyle NNZ</math>, przy czym
arytmetycznych jest identyczna, odmienne wykorzystanie pamięci ''cache''
wynikające z organizacji dostępu do pamięci w  naszych algorytmach.
Przedyskutujmy to dokładniej.


<center><math>\displaystyle
'''Algorytm ijk'''
A_{I[k], J[k]} = V[k], \qquad\forall k = 0,\ldots, NNZ-1.
 
</math></center>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
/* ijk */
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
for (k = 0; k < N; k++)
C[i*N+j] += A[i*N+k]*B[k*N+j];
</pre></div>
   
   
[[Image:MNaij-matrix-format.png|thumb|400px||Format AIJ]]
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>.


W tym formacie wprowadzane macierze rzadkie do Octave'a i MATLABa:
Odwołując się w najgłębszej pętli do kolejnych elementów macierzy <math>\displaystyle A</math> <strong>oraz</strong> <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 wielokrotnością 512: odwołując się
do kolejnych <code>B[k*N+j]</code>, <code>k</code> = <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 <strong>bez</strong> pamięci ''cache''.
 
'''Algorytm ikj'''
 
Różni się on od poprzedniego jedynie
kolejnością dwóch wewnętrznych pętli:


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
A <nowiki>=</nowiki> sparse(I,J,V,N,N);
/* ikj */
for (i = 0; i < N; i++)
for (k = 0; k < N; k++)
for (j = 0; j < N; j++)
C[i*N+j] += A[i*N+k]*B[k*N+j];
</pre></div>
</pre></div>
   
   
====Format spakowanych kolumn (wierszy)====
Okazuje się, że taka prosta zmiana dramatycznie poprawia sytuację, zmniejszając
liczbę ''cache misses''!


Format współrzędnych nie narzucał żadnego uporządkowania elementów macierzy
'''Algorytm bikj()'''
--- można było je umieszczać w dowolnej kolejności. Narzucenie sensownego
porządku mogłoby wspomóc realizację wybranych istotnych operacji na macierzy,
na przykład, aby wygodnie było realizować działanie (prawostronnego) mnożenia
macierzy przez wektor,  wygodnie byłoby przechowywać elementy macierzy ''wierszami''. Tak właśnie jest zorganizowany format spakowanych wierszy,
''Compressed Sparse Row (CSR) ''. Analogicznie jest zdefiniowany format
spakowanych kolumn,
''Compressed Sparse Column (CSC) '', którym zajmiemy się bliżej.


Podobnie jak w przypadku formatu współrzędnych, macierz w formacie CSC jest
Algorytm bikj(16) jest prostą wersją algorytmu blokowego operującego w sposób
przechowywana w postaci trzech wektorów: <code>AV</code> jest wektorem typu
"ikj" na blokach macierzy wymiaru <math>\displaystyle 16\times 16</math>:
<code>double</code> o długości <math>\displaystyle NNZ</math>, zawierającym ''kolejne'' niezerowe elementy
macierzy <math>\displaystyle A</math> wpisywane ''kolumnami'', <code>AI</code> jest wektorem typu <code>int</code>
o długości <math>\displaystyle NNZ</math>, zawierającym numery wierszy macierzy <math>\displaystyle A</math>, odpowiadających
elementom z <code>AV</code>. Natomiast zamiast tablicy <code>J</code>, jak to było w
formacie współrzędnych, mamy krótszy wektor typu <code>int</code>, <code>AP</code>, o
długości <math>\displaystyle M</math>, zawierający na <math>\displaystyle j</math>-tym miejscu indeks pozycji w <code>AV</code>, od
którego rozpoczynają się w <code>AV</code> elementy <math>\displaystyle j</math>-tej kolumny macierzy <math>\displaystyle A</math>.


[[Image:MNcsc-matrix-format.png|thumb|400px||Format CSC]]
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
/* bikj(16) */
for (i = 0; i < N; i+=16)
for (k = 0; k < N; k+=16)
for (j = 0; j < N; j+=16)
for (ii = i; ii < i+15; ii++)
for (kk = k; kk < k+15; kk++)
for (jj = j; jj < j+15; jj++)
C[ii*N+jj] += A[ii*N+kk]*B[kk*N+jj];


Mamy więc zależność, przy założeniu, że <math>\displaystyle AP[0]=0</math>,
</pre></div>
<center><math>\displaystyle  
A_{AI[AP[j]+k],j+1} = AV[AP[j]+k], \qquad j = 0,\ldots,M-1, \, k =
(podobnie działał algorytm bikj(32), tym razem na blokach <math>\displaystyle 32\times 32</math>).
0,\ldots,AP[j+1]-1.
</math></center>


Taki (z drobnymi modyfikacjami) format macierzy wykorzystują np. pakiety ARPACK
Wadą poprzedniego algorytmu, ikj, było to, że w dwu zewnętrznych pętlach powracał do
i UMFPACK.
wszystkich <math>\displaystyle N^2</math> wartości <math>\displaystyle C</math> i <math>\displaystyle B</math>, przecząc zasadzie <strong>lokalności  w
czasie</strong>. Wydzielenie w algorytmie bijk(16) operacji na blokach macierzy ma na celu uniknięcie tego
problemu.


====Format diagonalny====
'''Algorytmy DGEMM i ATLAS DGEMM'''


Znacznie mniej uniwersalny niż poprzednie i dlatego rzadziej spotykany. Kolejne
Algorytm DGEMM to był algorytm mnożenia macierzy z pakietu BLAS --- jest to
diagonale macierzy przechowujemy w kolejnych wierszach macierzy <math>\displaystyle P\times N</math>,
właśnie profesjonalny algorytm blokowy, ale niezoptymalizowany na naszą
gdzie <math>\displaystyle P</math> jest liczbą niezerowych diagonal w <math>\displaystyle A\in R^{N\times N}</math>.
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> = 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.


Szczególnie wygodny do reprezentacji macierzy taśmowych. Wykorzystywany m.in.
==Reprezentacja macierzy gęstych==
przez funkcję LAPACKa <code>DGBSV</code> służącą rozwiązywaniu równań z macierzami
taśmowymi.


===Metody bezpośrednie===
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. 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 teraz 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.


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
W Fortranie, elementy macierzy są przechowywane w pamięci kolumnami, tzn. jeśli
<span  style="font-variant:small-caps;">Przykład: Strzałka Wilkinsona</span>  
mamy do czynienia z macierzą prostokątną <math>\displaystyle n\times m</math> o elementach <math>\displaystyle a_{ij}</math>,
<div class="solution">
<math>\displaystyle i=1\ldots n</math>, <math>\displaystyle j=1\ldots m</math>,


Rozważmy układ równań z macierzą postaci
<center><math>\displaystyle
\begin{pmatrix}
a_{11} & \cdots & a_{1m}\\
\vdots &        & \vdots\\
a_{n1} & \cdots & a_{nm}
\end{pmatrix} .
</math></center>


<center><math>\displaystyle A = \begin{pmatrix}  
to  kolejne miejsca w przestrzeni adresowej
* & * & * & \cdots & * \\
zajmują elementy
* & * &        &  & \\
<center><math>\displaystyle a_{11}, a_{21},\ldots, a_{n1}, a_{12}, a_{22}, \ldots, a_{n2},
* &  & *      &  &  \\
\ldots a_{nm}.
\vdots & & & \ddots  & \\
* &  &      &  & *
\end{pmatrix}  
</math></center>
</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.


gdzie <math>\displaystyle *</math> oznacza jakiś niezerowy element.
To zaś powoduje kolejne komplikacje. Otóż w procedurach numerycznych bardzo
Łatwo sprawdzić, że chociaż wyjściowa macierz jest rozrzedzona, to zastosowanie do niej eliminacji Gaussa powoduje, że w wyniku
często pragniemy  dynamicznie przydzielać pamięć na tablice, to znaczy dopiero
dostajemy ''gęste'' czynniki rozkładu.
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''.  


Tymczasem wystarczy odwrócić kolejność równań i numerację niewiadomych (co dla
Dlatego przyjmiemy konwencję, że nie będziemy wcale korzystać z macierzy
macierzy jest równoznaczne z odwróceniem porządku wierszy i kolumn, korzystając
dwu- i więcejwymiarowych, a elementy macierzy zapiszemy do jednego odpowiednio
z pewnej (jakiej?) macierzy permutacji <math>\displaystyle P</math>):
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>.


<center><math>\displaystyle \widetilde{A} = PAP^T = \begin{pmatrix}
Mając pełną dowolność wyboru sposobu ułożenia elementów macierzy w wektorze,
* &        &  & & *\\
wybierzemy zazwyczaj układ kolumnowy (czyli fortranowski) (pamiętajmy wszak, że
  &  \ddots  & & & \vdots\\
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
* & \cdots & * & * & *
potencjalnie uprościć nam optymalizację "książkowych" algorytmów, których
\end{pmatrix}
większość była i często nadal jest pisana z myślą o implementacji w Fortranie.
</math></center>
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>.


Wtedy okazuje się, że rozkład naszej macierzy nie powoduje już wypełnienia ''
Poniżej pokazujemy przykładowy fragment kodu realizującego opisaną wyżej ideę.
fill-in ''
czynników rozkładu!
</div></div>


Właśnie na tym polega główny problem w rozwiązywaniu układów z macierzami
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
rzadkimi metodami bezpośrednimi: maksymalnie wykorzystać rozrzedzenie macierzy
#define N 10
tak, by czynniki rozkładu były możliwie mało wypełnione. Albowiem wiedząc to,
#define IJ(i,j,n) ((i)-1+((j)-1)*(n))
będziemy mogli ograniczyć się jedynie do fizycznego wyznaczenia wartości ''niezerowych'' elementów macierzy rozkładu. Ponadto, wymagania pamięciowe
#include <stdio.h>
algorytmu nie będą istotnie wykraczać ponad ilość pamięci potrzebnej na przechowanie
danych (macierzy).


W ogólnym przypadku rozwiązanie takiego zadania jest trudne i większość
int main(void)
algorytmów opiera się na pewnych heurystykach, które warto wspomóc wcześniejszą
{
analizą konkretnego układu równań, który mamy rozwiązać. Najczęściej dąży się do
double *matrix, *ptr;
takiego przenumerowania równań i niewiadomych, by w efekcie z góry przewidzieć,
int i,j;
gdzie wystąpią zera w macierzach rozkładu --- i, by takich zer było jak
najwięcej (by wypełnienie było jak najmniejsze)! Na architekturach z pamięcią
hierarchiczną dąży się także do tego, by w trakcie rozkładu można było korzystać
z BLAS3, a więc permutacje wierszy i kolumn macierzy muszą to także brać pod
uwagę (tzw. metody wielofrontowe).


Stosuje się kilka strategii wyznaczania korzystnych permutacji
matrix = (double *)malloc(N*N*sizeof(double));
''reorderingu '', z których warto wymienić algorytmy typu AMD i techniki
podziału grafów na (prawie) rozłączne składowe (METIS). Stosują je biblioteki
takie jak UMFPACK, czy HSL/MUMPS.


W Octave mamy do dyspozycji także kilka procedur generujących takie permutacje,
ptr = matrix;
w tym: <code>colamd</code> (AMD dla macierzy niesymetrycznych) oraz <code>symamd</code>
(AMD dla macierzy symetrycznych). Większy wybór oferuje MATLAB, jednak należy
bezwzględnie pamiętać o jednym: nie ma uniwersalnej metody reorderingu i dla
konkretnej macierzy może istnieć specjalna metoda, która da oszałamiające
rezultaty.


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
/* staramy się jak naczęściej odwoływać się do elementów macierzy kolumnowo */
<span  style="font-variant:small-caps;">Przykład: Wypełnienie pewnej macierzy w zależności od użytego algorytmu</span>
<div class="solution">


Rozważmy macierz pochodzącą z kolekcji
for (j=1; j<=N; j++)
[http://www.cise.ufl.edu/research/sparse/matrices/Boeing  Tima Davisa], dotyczącą pewnego zadania
for (i=1; i<=N; i++)
mechaniki strukturalnej jakie pojawiło się u Boeinga. Jest to macierz
{
symetryczna, dodatnio określona, wymiaru 8032.
*ptr = i+2*j; /* przypisanie wartości elementowi macierzy */  
ptr++; /* przejście do kolejnego elementu */
}
/* jeszcze raz to samo, ale w inny, bardziej czytelny, choć mniej optymalny
sposób */


[[Image:MNsparseA.png|thumb|400px||Struktura niezerowych elementów macierzy.]]
for (j=1; j<=N; j++)
for (i=1; i<=N; i++)
{
matrix[IJ(i,j,N)] = i+2*j;
}
/* dla wydruku, odwołujemy się do elementów macierzy wierszowo */
for (i=1; i<=N; i++)
{
for (j=1; j<=N; j++)
fprintf(stderr,"&#37;5.2g ", matrix[IJ(i,j,N)]);
fprintf(stderr,"\n");
}


Jest to macierz silnie rozrzedzona, ponieważ ma tylko 355460 niezerowych
free(matrix);
elementów (czyli wypełnienie to tylko 0.5 procent).
return(0);
}
</pre></div>
<!--
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.


Zobaczymy, jak w zależności od użytego algorytmu permutacji kolumn i
Za drugim razem chcemy wydrukować naszą macierz na ekranie, musimy więc
wierszy poradzi sobie algorytm rozkładu Cholesky'ego.
odwoływać się do kolejnych <strong>wierszy</strong> 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.
-->
Dodajmy, że opisane podejście
nie jest niczym nowym w praktyce numerycznej i jest stosowane w wielu dobrych
bibliotekach numerycznych.


[[Image:MNsparsechol.png|thumb|400px||Czynnik rozkładu Cholesky'ego <math>\displaystyle L</math> wykonanego
==Włączenie zewnętrznej biblioteki fortranowskiej
standardowym algorytmem. Czas rozkładu: 0.892013]]
do programu==


[[Image:MNsparsecholcolamd.png|thumb|400px||Czynnik rozkładu Cholesky'ego <math>\displaystyle L</math> z reorderingiem
Istnieje prosty sposób wykorzystania gotowych pakietów
COLAMD. Czas rozkładu: 0.813038]]
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>.


[[Image:MNsparsecholsymamd.png|thumb|400px||Czynnik rozkładu Cholesky'ego <math>\displaystyle L</math> z reorderingiem
Napiszemy program, który będzie liczył normę zadanego
SYMAMD. Czas rozkładu: 0.487683s. Prawie dwa razy
wektora, korzystając z funkcji <code>DNRM2</code> biblioteki
szybciej niż bez reorderingu, chociaż i tak wskutek wzrostu wypełnienia macierzy
[http://www.netlib.org/blas  BLAS].  
w dolnym trójkącie mamy aż 2 procent niezerowych elementów.]]


[[Image:MNsparsecholsymrcm.png|thumb|400px||Czynnik rozkładu Cholesky'ego <math>\displaystyle L</math> z odwrotnym reorderingiem
Najpierw musimy zorientować się, jak wygląda schemat wywołania takiej funkcji w
Cuthill-McKee. Czas rozkładu: 0.845928]]
Fortranie. Otóż funkcja wygląda następująco:


[[Image:MNsparsecholcolperm.png|thumb|400px||Czynnik rozkładu Cholesky'ego <math>\displaystyle L</math> z jeszcze
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
innym (bardzo tanim, ale jak widać czasem zupełnie złym) reorderingiem. Czas rozkładu: 5.947936]]
      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 := sqrt( x'*x )
*
i tak dalej...
</pre></div>
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


Na zakończenie popatrzmy, jak ważne jest spostrzeżenie symetrii macierzy:
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
norm( X(1:INCX:N) )
</pre></div>
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ć <strong>prototyp</strong> tej funkcji?


[[Image:MNsparseLlu.png|thumb|400px||Rozkład LU, czynnik <math>\displaystyle L</math> (bez reorderingu). Czas rozkładu LU: 1.696018s. Nieakceptowalny, podobnie
Przede wszystkim, zacznijmy od nazwy. W przypadku kompilatora
jak drastyczne wypełnienie macierzy.]]
<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>").


[[Image:MNsparseUlu.png|thumb|400px||Rozkład LU, czynnik <math>\displaystyle U</math> (bez reorderingu)]]
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 -- <strong>wskaźnik</strong> 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: każdy argument funkcji fortranowskiej zastępujemy <strong>wskaźnikiem</strong> do odpowiedniego typu:


Jak widać, w naszym przypadku standardowe algorytmy (SOLAMD i SYMAMD) poradziły sobie całkiem
{| border=1
nieźle, chociaż wypełnienie i tak znacząco wzrosło. Zapewne algorytm oparty na
|+ <span style="font-variant:small-caps"> </span>
podziale grafu na prawie rozłączne składowe mógłby tu jeszcze lepiej zadziałać.
|-
|
Fortran 77  ||  C
|-
|
INTEGER  ||  int
|-
| REAL  ||  float
|-
| DOUBLE PRECISION  ||  double
|-
| COMPLEX  ||  struct { float Re, Im; }
|-
| DOUBLE COMPLEX  ||  struct { double Re, Im; }
|-
| CHARACTER  ||  char
|-
|


</div></div>
|}


===Macierze specjalne===
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:


Należy pamiętać, że z reguły najlepsze wyniki uzyskuje się, gdy konkretny
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
algorytm dobierze się do konkretnej macierzy. W zastosowaniach pojawiają się
m.in. macierze rzadkie o bardzo szczególnej strukturze, dla nich warto stosować
double dnrm2_(int* N, double* X, int* INCX);
wyspecjalizowane algorytmy.
</pre></div>
No to wykorzystajmy naszą funkcję:


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<span style="font-variant:small-caps;">Przykład: Macierze taśmowe</span>
   
<div class="solution">
/* Wykorzystanie funkcji DNRM2 fortranowskiej biblioteki BLAS w programie w C*/
#include <stdio.h>
double dnrm2_(int*,double*,int*);


Macierz <math>\displaystyle A</math> taka, że dla <math>\displaystyle |i-j| \geq d</math>, <math>\displaystyle a_{ij} = 0</math>, nazywamy macierzą taśmową
int main(void)
z rozstępem <math>\displaystyle d</math>, o szerokości pasma <math>\displaystyle 2d+1</math>.
{
int n, incx=1;
double x[3]= {0,1,2};
n = 3;
printf("Norma podanego wektora: &#37;e\n", dnrm2_(&n, x, &incx));


Łatwo sprawdzić, że algorytm eliminacji Gaussa (bez wyboru elementu głównego)
return(0);
nie spowoduje dodatkowego wypełnienia w takiej macierzy (a więc da się wykonać w
}
miejscu). W przypadku konieczności wyboru elementu głównego, pesymistyczne
</pre></div>
oszacowanie rozstępu macierzy rozkładu LU jest równe <math>\displaystyle \max\{2d,N\}</math> --- tak
   
więc, dla niezbyt dużych <math>\displaystyle d</math> wciąż wynikowa macierz jest taśmowa.  
Zwróćmy uwagę na sposób kompilacji tego programu:


W szczególności, gdy macierz jest taśmowa z pasmem o rozstępie <math>\displaystyle d</math> i jednocześnie
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
diagonalnie dominująca, wtedy rozkład LU takiej macierzy da się wykonać w
miejscu, kosztem <math>\displaystyle O(d\,N)</math>, czyli liniowym.
gcc -o testblas testblas.c -lblas -lgfortran -lm
</pre></div>
oprócz biblioteki BLAS, co
naturalne, musimy dołączyć jeszcze bibliotekę matematyczną (może być
wykorzystywana przez BLAS!) oraz, co bardzo ważne,  specjalną bibliotekę:
<code>gfortran</code>, umożliwiającą koegzystencję Fortranu i
C.  


W LAPACKu zaimplementowano szybki solver równań z macierzami taśmowymi,
====Funkcja fortranowska z argumentem macierzowym====
<code>DGBSV</code>, wymagający skądinąd specjalnego sposobu
przechowywania macierzy, wykorzystyjącego format diagonalny.
</div></div>


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
Należy szczególną uwagę zwrócić na argumenty macierzowe funkcji w Fortranie,
<span  style="font-variant:small-caps;">Przykład: Macierz trójdiagonalna</span>
gdyż bardzo łatwo tu o pomyłkę, którą potem trudno wychwycić. Aby niepotrzebnie
<div class="solution">
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>:


Szczególnym przypadkiem macierzy taśmowych są macierze trójdiagonalne, tzn. taśmowe o
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
rozstępie <math>\displaystyle d=1</math>:
SUBROUTINE FILLMATRIX ( M, N, MATRIX )
INTEGER M,N
DOUBLE PRECISION MATRIX(M, N)
DO 10 I=1,M
DO 20 J=1,N
MATRIX(I,J) = I+2*J
20 CONTINUE
10 CONTINUE
END
</pre></div>
Nawet osoby nie znające Fortranu domyślą się, że wynikiem działania naszej
funkcji, np. dla <math>\displaystyle M=2</math>, <math>\displaystyle N=5</math>, będzie macierz


<center><math>\displaystyle  
<center><math>\displaystyle  
A =  
\lstF{MATRIX} =  
\begin{pmatrix}  
\begin{pmatrix}  
a_1 & c_1 & & & \\
3 & 5 & 7 & 9 & 11 \\
b_2 & a_2 & c_2      &   & \\
4 & 6 & 8 & 10 & 12
  &  b_3 & a_3  & \ddots &  \\
  & & \ddots & \ddots  & c_{N-1}\\
  &  &      & b_N  & a_N
\end{pmatrix}  
\end{pmatrix}  
</math></center>
</math></center>


Zadanie rozwiązywania równań z taką macierzą
Naturalnie, możemy wywołać ją wprost z programu w C przy użyciu poprzednio
<center><math>\displaystyle
poznanej techniki i następującego kodu (tym razem prototyp funkcji
Ax = e
<code>fillmatrix_</code> umieszczamy w osobnym pliku nagłówkowym <code>ffortran.h</code>,
</math></center>
gdzie mamy zamiar kolekcjonować prototypy w C lokalnie wykorzystywanych funkcji
fortranowskich):


jest tak często spotykane, że
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
warto przytoczyć algorytm w całej okazałości --- popularnie zwany algorytmem
przeganiania.
/*Wykorzystanie funkcji fortranowskiej operującej na macierzy.
Zwróćmy uwagę na sposób użycia argumentu macierzowego*/
#include <stdio.h>
#include <stdlib.h>
int fillmatrix_(int *, int *, double *);


Zacznijmy jednak od stwierdzenia, kiedy taka macierz nie wymaga wyboru elementu
int main()
głównego:
{
int MM, NN, i, j;
double *A;
  MM = 2; NN = 5;
A = (double *)malloc(MM*NN*sizeof(double));


{{stwierdzenie|||
  fillmatrix_( &MM, &NN, A );
Jeśli macierz <math>\displaystyle A</math> ma ''słabo'' dominującą przekątną, tzn.


<center><math>\displaystyle |a_i|\,\ge\,|b_i|\,+\,|c_i|,\qquad 1\le i\le N,
printf("\nKolejne elementy wektora A:\n\n"); 
</math></center>
  for ( i = 0; i < NN*MM ; i++ ){
printf("&#37;e\n", A[i] );
}


(<math>\displaystyle b_1=0=c_n</math>) i przynajmniej dla jednego indeksu "<math>\displaystyle i</math>" mamy powyżej
printf("\nWektor A zinterpretowany jako macierz:\n\n"); 
ostrą nierówność "<math>\displaystyle ></math>", to algorytm przeganiania jest wykonalny bez
for ( j = 0 ; j < MM ; j++ )
przestawień wierszy. Ponadto wymaga on <math>\displaystyle 7n</math> operacji arytmetycznych,
{
a więc jest prawie optymalny.
  for ( i = 0; i < NN ; i++ )
}}
  printf("&#37;e ", A[i*MM+j] );
printf("\n");
  }


{{algorytm|Metoda przeganiania||
  free( A );
<pre>


<math>\displaystyle d_1</math> <nowiki>=</nowiki> <math>\displaystyle a_1</math>;
  return(0);
<math>\displaystyle f_1</math> <nowiki>=</nowiki> <math>\displaystyle e_1</math>;
}
for (i <nowiki>=</nowiki> 2; i <<nowiki>=</nowiki> N; i++)
</pre></div>
{
<math>\displaystyle l</math> <nowiki>=</nowiki> <math>\displaystyle b_i</math>/<math>\displaystyle a_{i-1}</math>;
Zauważmy, że mimo iż funkcja fortranowska odwołuje się do macierzy <strong>dwuwymiarowej</strong>, jako argument jej wywołania w C przekazujemy <strong>tablicę
<math>\displaystyle d_i</math> <nowiki>=</nowiki> <math>\displaystyle a_i</math> - <math>\displaystyle l</math> * <math>\displaystyle c_{i-1}</math>;
jednowymiarową</strong> odpowiedniej wielkości.
<math>\displaystyle f_i</math> <nowiki>=</nowiki> <math>\displaystyle e_i</math> - <math>\displaystyle l</math> * <math>\displaystyle f_{i-1}</math>;
}  
<math>\displaystyle x_1</math> <nowiki>=</nowiki> <math>\displaystyle f_N</math>;
for (i <nowiki>=</nowiki> N-1; i ><nowiki>=</nowiki> 1; i--)
<math>\displaystyle x_i</math> <nowiki>=</nowiki> <math>\displaystyle f_i</math> - <math>\displaystyle c_i</math> * <math>\displaystyle x_{i+1}</math>;
</pre>}}


</div></div>
==BLAS, LAPACK i ATLAS==


===Stacjonarne metody iteracyjne===
W zadaniach dotyczących macierzy gęstych, warto skorzystać z klasycznego tandemu
bibliotek: [http://www.netlib.org/blas  BLAS] ''Basic Linear Algebra Subprograms ''
oraz [http://www.netlib.org/lapack  LAPACK] ''Linear Algebra PACKage ''. 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 [http://math-atlas.sourceforge.net  ATLAS]a,  . 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),  <strong>ponaddziesięciokrotne przyspieszenie</strong> 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>.


Gdy macierz jest rozrzedzona, mnożenie takiej macierzy przez wektor jest tanie
Aby osiągnąć największe przyspieszenie, bibliotekę ATLAS należy skompilować
(koszt jest proporcjonalny do liczby ''niezerowych'' elementów macierzy).
samemu na własnej (<strong>nieobciążonej</strong> w trakcie kompilacji!) architekturze. W plikach
Dlatego, jeśli możemy zadowolić się rozwiązaniem przybliżonym układu, a w zamian
<code>Makefile</code> ATLASa brak jednak opcji instalacji bibliotek w standardowych
osiągnąć je tanim kosztem, warto rozważyć metody iteracyjne.
miejscach --- trzeba zrobić to samemu.


Najprostsze metody iteracyjne (najprostsze w analizie i implementacji, ale ---
BLAS i LAPACK są często wykorzystywane przez inne biblioteki numeryczne,
jak można się domyślić --- w praktyce najmniej efektywne) polegają na rozkładzie
na nich opierają się również funkcje macierzowe w Octave i MATLABie.
macierzy na część "łatwo odwracalną" i "resztę". Dokładniej,
Optymalizowane biblioteki BLAS i LAPACK dla swoich architektur oferują
jeśli <math>\displaystyle A = M - N</math>, przy czym <math>\displaystyle M</math> też nieosobliwa, to równanie <math>\displaystyle Ax = b</math> można zapisać jako
producenci procesorów Intel (biblioteka [http://www.intel.com/cd/software/products/asmo-na/eng/perflib/mkl  MKL]) oraz AMD (biblioteka
zadanie punktu stałego
[http://developer.amd.com/acml.aspx  ACML])


<center><math>\displaystyle Mx = Nx + b,
BLAS  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>
</math></center>


inaczej:
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
<center><math>\displaystyle x = M^{-1}(Nx + b),
przez wektor
<center><math>\displaystyle  
y \leftarrow \alpha A x + y
</math></center>
</math></center>


i zastosować doń [[Uzupe�nij: metodę iteracji prostej Banacha]]:
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.
<center><math>\displaystyle x_{k+1} = M^{-1}(Nx_k + b).
wykorzystywać instrukcje wektorowe nowoczesnych procesorów;
</math></center>
* BLAS Level 3 -- operacje typu macierz--macierz, np. mnożenie dwóch
 
macierzy:
Takie metody nazywamy stacjonarnymi metodami iteracyjnymi.
 
Aby przeanalizować zbieżność takiej metody, warto rozpatrzyć
przypadek ogólniejszy
 
<center><math>\displaystyle  
<center><math>\displaystyle  
  x_k\,=\,B x_{k-1}\,+\, c,
C \leftarrow \alpha A\cdot B + C
</math></center>
</math></center>


dla pewnej macierzy <math>\displaystyle B</math> wymiaru <math>\displaystyle n\times n</math> i wektora
W związku z tym, że operacje macierzowe wymagają wykonania <math>\displaystyle O(N^3)</math> działań
<math>\displaystyle c\inR^n</math> (dla stacjonarnej metody iteracyjnej, <math>\displaystyle B=M^{-1}N</math> oraz <math>\displaystyle c=M^{-1}b</math>).  
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ą.


W tym przypadku
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 <strong>blokach</strong>
macierzy, zamiast na jej pojedynczych elementach.


<center><math>\displaystyle x_k- x^*\,=\,B^k( x_0- x^*),  
Większość użytkowników BLAS nie będzie jednak miała potrzeby pisania własnych
</math></center>
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
, który
intensywnie i skutecznie wykorzystuje podprogramy BLAS.


a stąd i z nierówności <math>\displaystyle \|B^k\|\le\|B\|^k</math>, mamy
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;


<center><math>\displaystyle \| x_k- x^*\|\,\le\,
;
          \|B\|^k\| x_0- x^*\|.
<code>RR</code> oznacza rodzaj zadania, np. GE oznacza ''GEneral '', czyli zadanie ogólne
</math></center>
(praktycznie bez założeń), a SY oznacza ''SYmmetric '', czyli zadanie symetryczne;


Warunkiem dostatecznym zbieżności iteracji prostych
;
jest więc <math>\displaystyle \|B\|<1</math>. Okazuje się, że warunkiem koniecznym i dostatecznym
<code>FF</code> wreszcie określa samo zadanie, np. SV oznacza
zbieżności tej iteracji dla dowolnego wektora startowego <math>\displaystyle x_0</math> jest  
''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 <strong>nie możemy zgadnąć</strong>, jaka jest nazwa procedury BLAS/LAPACKa,
która byłaby nam potrzebna,
najlepiej przejrzeć (przeszukać) strony internetowe tych pakietów w serwisie
Netlib.


<center><math>\displaystyle \rho = \max\{ |\lambda| : \lambda  \mbox{ jest wartością własną }  A\} < 1.
Zestawienie najczęściej wykorzystywanych procedur BLAS i LAPACKa przedstawiamy
</math></center>
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
Tak więc, metody oparte na iteracji prostej, będą zbieżne liniowo z ilorazem
osobnych procedur generujących rozkład LU oraz z innych, rozwiązujących układy
<math>\displaystyle \rho</math>.
trójkątne.


====Metoda Jacobiego====
{| border=1
|+ <span style="font-variant:small-caps"> </span>
|-
|


Biorąc <math>\displaystyle D = diag(A)</math>
Zadanie algebry liniowej  ||  Nazwa procedury BLAS/LAPACK
gdzie <math>\displaystyle diag(A)</math> jest macierzą diagonalną składającą się
|-
z wyrazów stojących na głównej przekątnej macierzy
|
<math>\displaystyle A</math>, układ <math>\displaystyle A x= b</math> jest równoważny układowi


<center><math>\displaystyle D x = N x +  b,
mnożenie wektora przez macierz  ||  DGEMV
</math></center>
|-
| 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
|-
|


a stąd (o ile na przekątnej macierzy <math>\displaystyle A</math> nie mamy zera)
|}
otrzymujemy metodę iteracyjną


<center><math>\displaystyle x_k\,=\,B x_{k-1}\,+\, c,
<!--  
</math></center>


gdzie <math>\displaystyle B=-D^{-1}N</math> i <math>\displaystyle  c=D^{-1} b</math>, zwaną
====Mnożenie macierz-wektor w BLAS====
''metodą Jacobiego''.


W metodzie Jacobiego warunek dostateczny zbieżności,  
Zacznijmy od prostej procedury BLAS Level 2, jaką jest mnożenie macierzy przez
<math>\displaystyle \|B\|<1</math>, jest spełniony np. wtedy, gdy macierz <math>\displaystyle A</math> ma
wektor. Wykorzystamy tzw. wzorcową implementację BLASów (niezoptymalizowaną)
dominującą przekątną, tzn. gdy
dostarczaną z dystrybucją np. Red Hat Linux. Jest to  biblioteka funkcji
fortranowskich.


Naszym zadaniem jest wykonanie operacji
<center><math>\displaystyle  
<center><math>\displaystyle  
  |a_{i,i}|\,>\,\sum_{j\neq i}|a_{i,j}|,\qquad \forall i = 1,\ldots, n.
y \leftarrow  \alpha A x + y,
</math></center>
</math></center>


Rzeczywiście, ponieważ wyraz <math>\displaystyle (i,j)</math> macierzy <math>\displaystyle D^{-1}N</math>
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>
wynosi <math>\displaystyle 0</math> dla <math>\displaystyle i=j</math> i <math>\displaystyle a_{i,j}/a_{i,i}</math> dla <math>\displaystyle i\neq j</math>, to
współrzędnych.
 
<center><math>\displaystyle \aligned \lefteqn{\|D^{-1}N\|_\infty \;=\; \max_{1\le i\le n}
  \sum_{j=1,j\ne i}^n{|a_{i,j}|}/{|a_{i,i}|} } \\
    && =\;\max_{1\le i\le n}
      \sum_{j=1}^n{|a_{i,j}|}/{|a_{i,i}|}\,-\,1\;<\;1,
\endaligned</math></center>
 
przy czym ostatnia nierówność wynika z warunku diagonalnej dominacji.  


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
To zadanie realizuje procedura BLASów o nazwie
<span  style="font-variant:small-caps;">Przykład: Macierz Laplasjanu</span>  
<code>DGEMV</code>. W rzeczywistości, ta procedura wykonuje ogólniejsze zadanie
<div class="solution">
wyznaczania wektora


Macierz <math>\displaystyle N\times N</math>
<center><math>\displaystyle  
<center><math>\displaystyle  
L = \begin{pmatrix}
y \leftarrow \alpha B x + \beta y,
2 & -1 &      & \\
-1 & 2 & \ddots & \\
    & \ddots & \ddots & -1 \\
    &      &  -1 & 2
\end{pmatrix}
</math></center>
</math></center>


pojawia się w bardzo wielu zastosowaniach, także jako podzadanie w algorytmach
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
numerycznych. Ta macierz jest macierzą taśmową, symetryczną i dodatnio
razem argumentem macierzowym, jaki przekazujemy <code>DGEMV</code>, jest wyjściowa
okresloną, więc rozwiązanie układu równań z tą macierzą jest łatwe metodami
macierz <math>\displaystyle A</math>).
bezpośrednimi, kosztem <math>\displaystyle O(N)</math>. Ciekawe będzie, jak poradzą sobie z nią metody
iteracyjne.


Na przykład, w metodzie Jacobiego weźmiemy <math>\displaystyle D = 2I</math> oraz <math>\displaystyle N = L - D</math>. Obliczając
Jak wiemy, że jest możliwe bezpośrednie wykorzystanie
normę macierzy iteracji Jacobiego dostajemy <math>\displaystyle ||D^{-1}N||_\infty
biblioteki fortranowskiej w programie w C, jednak musimy pamiętać, iż macierze z
= 1</math>, co nie rozstrzyga jeszcze o jej (nie)zbieżności.
jakich ona skorzysta muszą być ułożone <strong>kolumnami</strong> w jednolitym bloku
pamięci.


Okazuje się, że są gotowe wzory na wartości własne macierzy <math>\displaystyle L</math>:  
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:


<center><math>\displaystyle \lambda_j = 4\sin^2 \left(\frac{j \pi }{2(N+1)} \right),
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
</math></center>
   
 
int dgemv_( char* TRANS,  
dla <math>\displaystyle 1 \leq j \leq N.</math>
int* M,  
 
int* N,  
i w konsekwencji, wartościami własnymi <math>\displaystyle D^{1}N = \frac{1}{2}N = \frac{1}{2}L -
double* ALPHA,  
I</math> są liczby <math>\displaystyle \mu_i = \frac{1}{2} \lambda_i - 1 > 0</math>.
double* A,   
Zatem <math>\displaystyle ||N|| = 1 - O(N^{-2})</math> i metoda, choć zbieżna, dla dużych <math>\displaystyle N</math> staje się
int* LDA,
zbieżna tak wolno, że w praktyce bezużyteczna.
double* X,
</div></div>
int* INCX,
 
double* BETA,  
Zaletą stacjonarnych metod iteracyjnych jest również ich prostota,  
double* Y,  
przez co są one łatwe do zaprogramowania.
int* INCY );
 
</pre></div>
====Złożoność stacjonarnych metod iteracyjnych====
 
Zastanówmy się teraz nad złożonością metod
iteracyjnych. Ponieważ możemy jedynie znaleźć pewne
przybliżenie rozwiązania dokładnego <math>\displaystyle  x^*</math>, przez
złożoność metody będziemy rozumieli koszt
kombinatoryczny obliczenia <math>\displaystyle x_k</math> z zadaną
dokładnością <math>\displaystyle \epsilon>0</math>. Dla uproszczenia założymy,  
że medoda jest zbieżna liniowo z ilorazem <math>\displaystyle \rho</math>.
Zauważmy, że aby zredukować błąd początkowy do
<math>\displaystyle \epsilon>0</math>, wystarczy wykonać <math>\displaystyle k=k(\epsilon)</math> iteracji, gdzie
<math>\displaystyle k</math> spełnia
 
<center><math>\displaystyle \rho^k\| x_0- x^*\|\,\le\,\epsilon,
</math></center>
 
czyli
 
<center><math>\displaystyle k\,\ge\,\frac{\log(1/\epsilon)-
    \log(1/\| x_0- x^*\|)}{\log(1/\rho)}.
</math></center>
 
Liczba ta zależy więc w istotny sposób od błędu
początkowego i (przede wszystkim) od współczynnika redukcji błędu
<math>\displaystyle \rho</math>, natomiast zależność od dokładności <math>\displaystyle \epsilon</math>
i wymiaru <math>\displaystyle n</math> układu jest dużo mniej istotna (w zadaniach praktycznych jednak
często
okazuje się, że... <math>\displaystyle \rho</math> zależy od <math>\displaystyle n</math>!).
 
Zakładając, że koszt jednej iteracji wynosi <math>\displaystyle c=c(n)</math>
(zwykle <math>\displaystyle c(n)</math> jest tym mniejszy, im mniejsza jest liczba
niezerowych elementów macierzy <math>\displaystyle A</math>), złożoność
metody jest proporcjonalna do
 
<center><math>\displaystyle c(n)\,\frac{\log(1/\epsilon)}{\log(1/\rho)}.
</math></center>
 
Stąd oczywisty wniosek, że metody iteracyjne warto
stosować zamiast metod bezpośrednich w przypadku gdy
* wymiar <math>\displaystyle n</math> układu <math>\displaystyle A x= b</math> jest "duży", oraz
* macierz <math>\displaystyle A</math> układu jest "rozrzedzona", tzn. ma stosunkowo niewielką liczbę elementów  niezerowych, np. proporcjonalną do <math>\displaystyle n</math>.
    
    
Układy o tych własnościach powstają często przy
Dla własnej wygody, a także dla przyszłego wykorzystania, umieścimy ten
numerycznym rozwiązywaniu równań różniczkowych
prototyp, razem z innymi przydatnymi drobiazgami (m.in. makro <code>IJ</code>
cząstkowych.  
dające wygodny dostęp do macierzy niezależny od jej wewnętrznej reprezentacji, a
także zmienne całkowite
<code>static int BLASONE = 1, BLASMONE = -1;</code>), w pliku
nagłówkowym <code>blaslapack.h</code>.


===Metody przestrzeni Kryłowa===
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>:


Zupełnie inny pomysł na realizację metody iteracyjnej przedstawiają metody przestrzeni
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Kryłowa, gdzie kolejne przybliżenie <math>\displaystyle x_k</math> dobiera się w taki sposób by
#include <stdio.h>
minimalizowało pewną miarę błędu na podprzestrzeni Kryłowa
#include "blaslapack.h"


<center><math>\displaystyle K_k =  \mbox{span} \{r,A_r,\ldots, A^{k-1}r \},
double* mmread(char *filename, int* N, int* M );
</math></center>


gdzie <math>\displaystyle r = b-Ax_0</math> jest residuum na początku iteracji. W zależności od wyboru
int main()
sposobu miary błędu, dostajemy inną metodę iteracyjną, takie jak CG, GMRES, PCR,
{
BiCG, i inne. Tutaj omówimy pokrótce tylko dwie najpopularniejsze: CG i GMRES.
int N, M, i, j;
double *A, *x, *y;
/* wczytujemy macierz z pliku w formacie MatrixMarket */
/* macierz jest reprezentowana w formacie kolumnowym  */
A = mmread("mbeacxc.mtx", &N, &M );
x = (double *)malloc(N*sizeof(double));
y = (double *)malloc(M*sizeof(double));
for (i = 1; i <= N; i++)
x[IJ(i,1,N)] = (double)i;


====CG====
/* obliczamy y = 5*A*x, korzystając z procedury BLAS L2: DGEMV */


Musimy dodatkowo założyć, że <math>\displaystyle A</math> jest symetryczna i dodatnio określona.
{
Kolejne przybliżenie <math>\displaystyle x_k</math> ma minimalizować błąd w normie energetycznej
char TRANS = 'N'; double ALPHA = 5.0, BETA = 0.0;
indukowanej przez <math>\displaystyle A</math>,
dgemv_(&TRANS, &N, &M, &ALPHA, A, &N,  x,  &BLASONE,
                        &BETA, y, &BLASONE );


<center><math>\displaystyle ||x_k -x||_A = \sqrt{(x_k -x)^TA(x_k -x)}
}
</math></center>
/* wydruk wyniku */
for (i = 1; i <= M; i++)
printf("&#37;E\n",y[IJ(i,1,M)]);
return(0);
}
</pre></div>
Zwróćmy uwagę na wykorzystanie "stałej" <code>BLASONE</code>, równej 1,
predefiniowanej w pliku <code>blaslapack.h</code>.


na przestrzeni afinicznej <math>\displaystyle x_0 + K_k</math>. Okazuje się (co nie jest oczywiste), że
-->
takie zadanie minimalizacji daje się bardzo efektywnie rozwiązać, skąd dostajemy
bardzo zwarty algorytm:
Programy korzystające z BLASów i LAPACKa kompilujemy
standardowo, pamiętając o dołączeniu na etapie linkowania używanych przez nas
bibliotek:


{{twierdzenie|Zbieżność CG jako metody bezpośredniej||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
 
}}
 
{{twierdzenie|Zbieżność CG jako metody iteracyjnej||
 
}}
 
====GMRES====
 
{{twierdzenie|Zbieżność GMRES jako metody bezpośredniej||
 
}}
 
{{twierdzenie|Zbieżność GMRES jako metody iteracyjnej||
 
}}
 
====Prekondycjoning====
 
Zbieżność wszystkich poznanych metod iteracyjnych zależu od własności
spektralnych macierzy układu. Często w zastosowaniach pojawiające się macierze
mają niekorzystne własności spektralne (np. bardzo duży wskaźnik uwarunkowania)
przez co metody iteracyjne zbiegają na nich bardzo wolno.
 
Dlatego bardzo korzystne może być wstępne przetransformowanie układu
 
<center><math>\displaystyle Ax = b
</math></center>
 
z macierzą o niekorzystnych własnościach, do układu
 
<center><math>\displaystyle MAx = Mb,
</math></center>
 
gdzie macierz <math>\displaystyle MA</math> ma znacznie korzystniejsze własności z punktu widzenia
używanej metody
iteracyjnej.
Taką operację nazywamy ''prekondycjoningiem'', a macierz <math>\displaystyle M</math> --- macierzą
prekondycjonera.
 
Aby całość miała sens, macierz prekondycjonera <math>\displaystyle M</math> powinna:
* być łatwa w konstrukcji,
* być tania w mnożeniu przez wektor (głównym elementem każdej metody
iteracyjnej jest mnożenie macierzy przez wektor: <math>\displaystyle M\cdot (A \cdot x)</math>),
* macierz <math>\displaystyle MA</math> powinna mieć znacznie korzystniejsze własności z punktu widzenia
używanej metody
iteracyjnej.
   
   
Kilka ekstremalnych propozycji na macierz prekondycjonera to <math>\displaystyle M = I</math> (łatwa w
gcc -o testblas testblas.c -llapack -lblas -lgfortran -lm
konstrukcji i tania w mnożeniu, ale nic nie polepsza, niestety...) oraz
</pre></div>
<math>\displaystyle M=A^{-1}</math> (rewelacyjnie poprawia zbieżność metody iteracyjnej, dając zbieżność
w jednej iteracji, ale bardzo droga w konstrukcji i mnożeniu). Widać więc, że
należy poszukiwać czegoś pośredniego, co niskim kosztem przybliża działanie
macierzy odwrotnej.
 
Dlatego jednym z powszechniej stosowanych rodzajów prekondycjonerów są oparte na
zastosowaniu jednego kroku klasycznej metody iteracyjnej.
 
[[Image:MNpcgconv.png|thumb|400px||Zbieżność metody CG bez prekondycjonera oraz z prekondycjonerem
opartym na jednej iteracji (blokowej) metody Jacobiego.]]
 
Inne prekondycjonery stosują np. techniki tzw. niepełnego rozkładu macierzy,
albo --- w specyficznych przypadkach --- tzw. metody wielosiatkowe.
 
Okazuje się, że zarówno CG jak i GMRES da się zaimplementować tak, by w jednej
iteracji było konieczne tylko jedno mnożenie przez macierz prekondycjonera.

Wersja z 18:13, 1 wrz 2006

Pamięć hierarchiczna komputerów a algorytmy numeryczne

W poprzednim wykładzie sprawdziliśmy, jaki jest koszt obliczeniowy algorytmu eliminacji Gaussa. Jednak w obecnym świecie istotna jest nie tylko liczba operacji arytmetycznych, ale także koszt pobrania danych z pamięci. Za chwilę zobaczymy, że poprzez reorganizację kolejności obliczeń w algorytmie eliminacji Gaussa, możemy dostać algorytm (tzw. algorytm blokowy), którego implementacja, choć znacznie mniej czytelna niż powyżej, będzie znacznie szybsza!

Bez dostatecznie szybkiej pamięci, procesor -- zamiast liczyć -- będzie większość czasu czekał na dane, a jego efektywność drastycznie spadnie. Z niewielką przesadą można powiedzieć, że

W optymalizacji szybkości działania programu numerycznego,

obecnie cała walka idzie o to, by procesor przez cały czas miał co liczyć.

Szczególnie jest to widoczne w algorytmach, które wykonują bardzo dużo operacji na dużej liczbie danych --- a tak jest m.in. w algorytmach algebry liniowej takich jak mnożenie dwóch macierzy, czy rozwiązywanie układów równań liniowych: te algorytmy najczęściej operują na O(N2) danych i wykonują aż O(N3) działań.

Ponieważ szybka pamięć komputerowa jest jednocześnie bardzo droga, konstruktorzy komputerów osobistych stoją przed wykluczającymi się celami: z jednej strony powinni zapewnić użytkownikowi jak najwięcej pamięci, z drugiej zaś -- chcieliby zapewnić użytkownikowi jak najszybszą pamięć. Sprzeczność tych dążeń ujawnia się, gdy dołączyć do nich trzecie, ale zasadnicze wymaganie: całość ma być w rozsądnej cenie... Ostatecznie więc, z biegiem lat dramatycznie pogłębia się przepaść pomiędzy prędkością (podwajającą się, zgodnie z heurystycznym prawem Moore'a, co półtora roku) procesora, a prędkością pamięci RAM, do której procesor musi się odwoływać.

Powszechnie przyjętym sposobem pogodzenia tych sprzeczności jest pamięć hierarchiczna. Koncept polega na tym, że część pamięci, która najczęściej komunikuje się z procesorem, jest bardzo szybka (lecz jest jej relatywnie mało), natomiast pozostała pamięć jest wolniejsza, za to może jej być bardzo dużo.

W praktycznej realizacji, zamiast dwóch poziomów pamięci (mała-szybka i duża-wolna), w komputerze występuje wiele poziomów:

  • rejestry procesora
  • cache (pamięć podręczna) procesora
  • cache drugiego poziomu (ostatnio także wbudowywana do procesora)
  • pamięć operacyjna (RAM): główna pamięć komputera
  • pamięć zewnętrzna (np. twardy dysk)
  • pamięć masowa (CD-ROM, taśma streamera, itp.)

Efektywność działania komputera, zwłaszcza w wielkoskalowych obliczeniach numerycznych, bardzo istotnie zależy od skutecznego wykorzystania hierarchii pamięci tak, by jak największa część operacji była wykonywana na zmiennych znajdujących się w danej chwili w jak najszybszej pamięci procesora.

Kluczem do skutecznego wykorzystania hierarchii pamięci jest zasada lokalności w czasie i w przestrzeni:

  • Lokalność w czasie: Używać danego fragmentu pamięci intensywnie, ale rzadko.
  • Lokalność w przestrzeni (adresowej): W danej chwili, odnosić

się do adresów pamięci leżących blisko siebie.

Zachowanie zasady lokalności w czasie jest bardzo ważne dla efektywnego wykorzystania cache'a -- ze względu na małe rozmiary tej pamięci, wymiana zmiennych może być tam intensywna. Zasada lokalności w przestrzeni też jest ważna, przy czym nie tylko ze względu na efektywne wykorzystanie cache'a, ale także dla efektywnego wykorzystania pamięci wirtualnej.

Jak napisać kod źle wykorzystujący pamięć podręczną?

Choć programista nie ma bezpośredniego wpływu na opisane powyżej działania systemu operacyjnego i hardware'u (zarządzających, odpowiednio, pamięcią wirtualną i cache), to przez właściwe projektowanie algorytmów -- a zwłaszcza: ich właściwą implementację -- może spowodować, że jego programy nie będą zmuszały komputera do nieracjonalnych zachowań. Jak się okazuje, całkiem łatwo nieświadomie tworzyć programy przeczące zasadom efektywnego wykorzystania hierarchii pamięci. Na potwierdzenie zacytujmy klasyczny już przykład mnożenia dwóch macierzy.

W programie wykonujemy operację mnożenia dwóch macierzy 1024×1024 przy użyciu kilku matematycznie równoważnych algorytmów (nazwaliśmy je umownie ijk, ikj, bikj() --- nazwy pochodzą od sposobu organizacji pętli, zob. poniżej), zaimplementowanych w programie C, wykorzystującym technikę pozwalającą przechowywać macierze w pamięci komputera kolumnowo. Dla porównania zmierzyliśmy czas wykonania tej samej operacji przy użyciu wyspecjalizowanych bibliotek z pakietów BLAS (algorytm DGEMM) i ATLAS (algorytm ATLAS DGEMM). Oto jakie wyniki uzyskaliśmy dla obliczeń w arytmetyce podwójnej precyzji double na maszynie z procesorem AMD Duron i zegarem 1.1 GHz:

Algorytm || ijk || ikj || bikj(16) || bikj(32) || DGEMM || ATLAS DGEMM

Czas (s) || 320.49 || 24.28 || 8.68 || 30.45 || 25.72 || 2.58

Mflop/s || 10.06 || 132.67 || 371.11 || 105.79 || 125.24 || 1248.53

Jak widać, różnice pomiędzy --- podkreślmy, matematycznie równoważnymi --- algorytmami są bardzo znaczące; w szczególności algorytm ijk wydaje się nie do przyjęcia! Powodem różnic musi być, ponieważ liczba wykonanych operacji arytmetycznych jest identyczna, odmienne wykorzystanie pamięci cache wynikające z organizacji dostępu do pamięci w naszych algorytmach. Przedyskutujmy to dokładniej.

Algorytm ijk

 
/* ijk */
for (i = 0; i < N; i++)
	for (j = 0; j < N; j++)
		for (k = 0; k < N; k++)
			C[i*N+j] += A[i*N+k]*B[k*N+j];

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 double.

Odwołując się w najgłębszej pętli do kolejnych elementów macierzy A oraz B powodujemy, że przy odwoływaniu się do B, 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 B[k*N+j], k = 0N, 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.

Algorytm ikj

Różni się on od poprzedniego jedynie kolejnością dwóch wewnętrznych pętli:

 
/* ikj */
for (i = 0; i < N; i++)
	for (k = 0; k < N; k++)
		for (j = 0; j < N; j++)
			C[i*N+j] += A[i*N+k]*B[k*N+j];

Okazuje się, że taka prosta zmiana dramatycznie poprawia sytuację, zmniejszając liczbę cache misses!

Algorytm bikj()

Algorytm bikj(16) jest prostą wersją algorytmu blokowego operującego w sposób "ikj" na blokach macierzy wymiaru 16×16:

 
/* bikj(16) */
 for (i = 0; i < N; i+=16)
 	for (k = 0; k < N; k+=16)
 		for (j = 0; j < N; j+=16)
 			for (ii = i; ii < i+15; ii++)
 				for (kk = k; kk < k+15; kk++)
					for (jj = j; jj < j+15; jj++)
						C[ii*N+jj] += A[ii*N+kk]*B[kk*N+jj];

(podobnie działał algorytm bikj(32), tym razem na blokach 32×32).

Wadą poprzedniego algorytmu, ikj, było to, że w dwu zewnętrznych pętlach powracał do wszystkich N2 wartości C i B, przecząc zasadzie lokalności w czasie. Wydzielenie w algorytmie bijk(16) operacji na blokach macierzy ma na celu uniknięcie tego problemu.

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 rmax = 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.

Reprezentacja macierzy gęstych

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. 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 teraz 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ą n×m o elementach aij, i=1n, j=1m,

(a11a1man1anm).

to kolejne miejsca w przestrzeni adresowej zajmują elementy

a11,a21,,an1,a12,a22,,an2,anm.

Dla odmiany, C przechowuje w pamięci elementy macierzy wierszami, tzn. kolejno

a11,a12,,a1m,a21,a22,,a2m,anm.

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.

Dlatego 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 n×m będziemy zapisywali do wektora o długości nm.

Mając pełną dowolność wyboru sposobu ułożenia elementów macierzy w wektorze, wybierzemy zazwyczaj układ kolumnowy (czyli fortranowski) (pamiętajmy wszak, że niektóre biblioteki w C (np. 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 (i,j)-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 aij, i=1n, j=1m.

Poniżej pokazujemy przykładowy fragment kodu realizującego opisaną wyżej ideę.

#define N 10
#define IJ(i,j,n) ((i)-1+((j)-1)*(n))
#include <stdio.h>

int main(void)
{
double *matrix, *ptr;
int i,j;

matrix = (double *)malloc(N*N*sizeof(double));

ptr = matrix;

/* staramy się jak naczęściej odwoływać się do elementów macierzy kolumnowo */

for (j=1; j<=N; j++)
	for (i=1; i<=N; i++)
	{
*ptr = i+2*j; 	/* przypisanie wartości elementowi macierzy */ 
		ptr++;		/* przejście do kolejnego elementu */
	}
		
/* jeszcze raz to samo, ale w inny, bardziej czytelny, choć mniej optymalny
sposób */

for (j=1; j<=N; j++)
	for (i=1; i<=N; i++)
	{
		matrix[IJ(i,j,N)] = i+2*j;
	}
		
/* dla wydruku, odwołujemy się do elementów macierzy wierszowo */
for (i=1; i<=N; i++)
{
	for (j=1; j<=N; j++)
		fprintf(stderr,"%5.2g ", matrix[IJ(i,j,N)]);
	fprintf(stderr,"\n");
}

free(matrix);		
return(0);
}


Dodajmy, że opisane podejście nie jest niczym nowym w praktyce numerycznej i jest stosowane w wielu dobrych bibliotekach numerycznych.

==Włączenie zewnętrznej biblioteki fortranowskiej do programu==

Istnieje prosty sposób wykorzystania gotowych pakietów fortranowskich przez zewnętrzne programy w C: wystarczy skorzystać z genialnej biblioteki f2c lub jej modyfikacji na użytek kompilatora GCC, biblioteki gfortran.

Napiszemy program, który będzie liczył normę zadanego wektora, korzystając z funkcji DNRM2 biblioteki BLAS.

Najpierw musimy zorientować się, jak wygląda schemat wywołania takiej funkcji w Fortranie. Otóż funkcja wygląda następująco:

 
      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 := sqrt( x'*x )
*
i tak dalej...

Tak więc nasza funkcja obliczająca normę wektora ma trzy argumenty: N -- długość wektora (INTEGER), X -- wektor, którego długość chcemy obliczyć (tablica liczb DOUBLE PRECISION) oraz tajemniczy dodatkowy parametr INCX typu INTEGER -- jest to wartość skoku, określająca co który element wektora uwzględnić w obliczaniu normy: aby policzyć normę całego wektora, bierzemy INCX równe 1. Używając zapisu Octave, DNRM2 oblicza po prostu

 
norm( X(1:INCX:N) )

Kod obiektowy tej funkcji znajduje się już w bibliotece BLAS, zawartej w pliku libblas.a. 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 gcc/gfortran, nazwą funkcji do wykorzystania w C będzie dnrm2_ (tak! małymi literami i z przyrostkiem "_").

Jeśli zaś chodzi o argumenty, to zapewne co do drugiego nie będziemy mieli wątpliwości: jako wektor X przekażemy -- naturalnie -- wskaźnik do tablicy X (typu double), czyli po prostu: jej nazwę. Co z pozostałymi argumentami? Okazuje się, że reguła jest niezmienna: każdy argument funkcji fortranowskiej zastępujemy wskaźnikiem do odpowiedniego typu:

Fortran 77 || C

INTEGER || int

REAL float
DOUBLE PRECISION double
COMPLEX struct { float Re, Im; }
DOUBLE COMPLEX struct { double Re, Im; }
CHARACTER char

A więc pierwszym i trzecim argumentem funkcji dnrm2_ będą wskaźniki do int. Ponieważ funkcja DNRM2 zwraca w wyniku liczbę podwójnej precyzji, to ostatecznie prototyp naszej funkcji w C będzie następujący:

 
double dnrm2_(int* N, double* X, int* INCX);

No to wykorzystajmy naszą funkcję:

 
/* Wykorzystanie funkcji DNRM2 fortranowskiej biblioteki BLAS w programie w C*/
#include <stdio.h>
double dnrm2_(int*,double*,int*);

int main(void)
{
	int n, incx=1;
	double x[3]= {0,1,2};
	
	n = 3;
	printf("Norma podanego wektora: %e\n", dnrm2_(&n, x, &incx));

	return(0);
}

Zwróćmy uwagę na sposób kompilacji tego programu:

 
gcc -o testblas testblas.c -lblas -lgfortran -lm

oprócz biblioteki BLAS, co naturalne, musimy dołączyć jeszcze bibliotekę matematyczną (może być wykorzystywana przez BLAS!) oraz, co bardzo ważne, specjalną bibliotekę: gfortran, umożliwiającą koegzystencję Fortranu i C.

Funkcja fortranowska z argumentem macierzowym

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 M×N:

 
	SUBROUTINE FILLMATRIX ( M, N, MATRIX )
	INTEGER M,N
	DOUBLE PRECISION MATRIX(M, N)
	
	DO 10 I=1,M
		DO 20 J=1,N 
			MATRIX(I,J) = I+2*J
20		CONTINUE
10	CONTINUE
	END

Nawet osoby nie znające Fortranu domyślą się, że wynikiem działania naszej funkcji, np. dla M=2, N=5, będzie macierz

Parser nie mógł rozpoznać (nieznana funkcja „\lstF”): {\displaystyle \displaystyle \lstF{MATRIX} = \begin{pmatrix} 3 & 5 & 7 & 9 & 11 \\ 4 & 6 & 8 & 10 & 12 \end{pmatrix} }

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 fillmatrix_ umieszczamy w osobnym pliku nagłówkowym ffortran.h, gdzie mamy zamiar kolekcjonować prototypy w C lokalnie wykorzystywanych funkcji fortranowskich):

 
/*Wykorzystanie funkcji fortranowskiej operującej na macierzy. 
Zwróćmy uwagę na sposób użycia argumentu macierzowego*/
#include <stdio.h>
#include <stdlib.h>
int fillmatrix_(int *, int *, double *); 

int main()
{
	int MM, NN, i, j;
	double *A;
	
   	MM = 2; NN = 5;
	
	A = (double *)malloc(MM*NN*sizeof(double));

   	fillmatrix_( &MM, &NN, A );

	printf("\nKolejne elementy wektora A:\n\n");   
   	for ( i = 0; i < NN*MM ; i++ ){
		printf("%e\n", A[i] );
	}

	printf("\nWektor A zinterpretowany jako macierz:\n\n");   
	for ( j = 0 ; j < MM ; j++ )
	{
   		for ( i = 0; i < NN ; i++ )
   			printf("%e ", A[i*MM+j] );
		printf("\n");
   	}

   	free( A );

   	return(0);
}

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 oraz LAPACK Linear Algebra PACKage . 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, . 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 float i double i około pięciokrotne na zmiennych typu complex i double complex.

Aby osiągnąć największe przyspieszenie, bibliotekę ATLAS należy skompilować samemu na własnej (nieobciążonej w trakcie kompilacji!) architekturze. W plikach Makefile 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 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

yαx+y,

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

yαAx+y

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;

  • BLAS Level 3 -- operacje typu macierz--macierz, np. mnożenie dwóch

macierzy:

CαAB+C

W związku z tym, że operacje macierzowe wymagają wykonania O(N3) działań arytmetycznych przy O(N2) danych (gdzie N jest wymiarem macierzy), wykorzystanie zoptymalizowanych procedur BLAS Level 3 może znacząco przyspieszyć wykonywanie obliczeń na maszynach z pamięcią hierarchiczną.

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 , 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 PRRFF, gdzie

P 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;

RR oznacza rodzaj zadania, np. GE oznacza GEneral , czyli zadanie ogólne

(praktycznie bez założeń), a SY oznacza SYmmetric , czyli zadanie symetryczne;

FF 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.

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 T 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


Programy korzystające z BLASów i LAPACKa kompilujemy standardowo, pamiętając o dołączeniu na etapie linkowania używanych przez nas bibliotek:

 
gcc -o testblas testblas.c -llapack -lblas -lgfortran -lm