Przykre testy: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przykry (dyskusja | edycje)
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 44 wersji utworzonych przez 2 użytkowników)
Linia 1: Linia 1:
<!--  
<!--  
Konwertowane  z pliku LaTeX przez latex2mediawiki, zob. http://www.ii.uj.edu.pl/&nbsp;pawlik1/latex2mediawiki.php.
Konwertowane  z pliku LaTeX przez latex2mediawiki, zob. http://www.ii.uj.edu.pl/&nbsp;pawlik1/latex2mediawiki.php.
Linia 6: Linia 5:
-->
-->
   
   
=Pamięć hierarchiczna komputerów a algorytmy numeryczne=
<div style="background-color:#fff; border: 1em #eee solid; padding:1em; margin-bottom:1em;">
[http://www.mimuw.edu.pl/~przykry/Zamawiane/przedmiot.pdf Notatki w formacie PDF]
[http://www.mimuw.edu.pl/~przykry/Zamawiane/przedmiot-beamer.pdf Slajdy w formacie PDF]
[http://usosweb.mimuw.edu.pl/kontroler.php?_action=actionx:katalog2/przedmioty/pokazPrzedmiot(prz_kod:1000-5D96MN) Strona przedmiotu w USOSie]
</div>


W poprzednim wykładzie sprawdziliśmy, jaki jest koszt obliczeniowy algorytmu
=Wielkie układy równań liniowych=
eliminacji Gaussa. Jednak w obecnym świecie istotna jest nie tylko liczba
{{powrot |Metody numeryczne | do strony głównej
operacji arytmetycznych, ale także koszt pobrania danych z pamięci. Za chwilę
przedmiotu <strong>Metody numeryczne</strong>}}
zobaczymy, że poprzez <strong>reorganizację kolejności obliczeń</strong> w algorytmie
eliminacji Gaussa, możemy dostać algorytm (tzw. algorytm blokowy), którego
implementacja, choć znacznie mniej czytelna niż powyżej, będzie <strong>znacznie
szybsza!</strong>


Bez dostatecznie szybkiej pamięci procesor -- zamiast liczyć -- będzie
[[MN08LAB | Ćwiczenia do wykładu >>>]]
większość czasu czekał na dane, a jego efektywność drastycznie spadnie. Z
niewielką przesadą można powiedzieć, że


<blockquote  style="background-color: #fefeee; padding:1em;  margin-left,margin-right:2em;  margin-top,margin-bottom: 1em;">  w optymalizacji szybkości działania programu numerycznego,
obecnie cała walka idzie o to, by procesor przez cały czas <strong>miał co liczyć</strong>.
</blockquote>


Szczególnie jest to widoczne w algorytmach, które wykonują bardzo dużo operacji
==Wstęp==
na dużej liczbie
Wraz z coraz większymi modelami pojawiającymi się w praktyce obliczeniowej,
danych --- a tak jest m.in. w algorytmach algebry liniowej takich, jak mnożenie
coraz częściej zachodzi potrzeba rozwiązywania zadań algebry liniowej, w której
dwóch macierzy, czy rozwiązywanie układów równań liniowych: te algorytmy
macierze są co prawda wielkiego wymiaru, ale najczęściej <strong>rozrzedzone</strong>, to
najczęściej operują na <math>\displaystyle O(N^2)</math> danych i wykonują aż <math>\displaystyle O(N^3)</math> działań.
znaczy jest w nich bardzo dużo zer. Bardzo często zdarza się, że macierz
wymiaru <math>N</math> ma tylko <math>O(N)</math> niezerowych elementów. Wykorzytanie tej specyficznej
własności macierzy nie tylko prowadzi do algorytmów istotnie szybszych od ich
analogów dla macierzy gęstych (to znaczy takich, które (w założeniu) mają <math>N^2</math>
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!


Ponieważ szybka pamięć komputerowa jest jednocześnie bardzo droga,
Jednym ze szczególnie ważnych źródeł układów równań z macierzami rozrzedzonymi są np. <strong>równania różniczkowe cząstkowe</strong> (a więc np. modele pogody, naprężeń w konstrukcji samochodu, przenikania kosmetyków do głębszych warstw skóry, itp.).
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... Z biegiem lat 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ęć
Modele wielostanowych systemów kolejkowych (np. routera obsługującego wiele komputerów) także prowadzą do gigantycznych układów równań z macierzami rozrzedzonymi o specyficznej strukturze.
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
Z reguły zadania liniowe ''wielkiego wymiaru'' będą miały strukturę macierzy rozrzedzonej, gdyż najczęściej związki pomiędzy niewiadomymi w równaniu nie dotyczą wszystkich, tylko wybranej grupy.
duża-wolna), w komputerze występuje wiele poziomów:
 
* rejestry procesora
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
* ''cache''(pamięć podręczna) procesora
<span  style="font-variant:small-caps;">Przykład: Macierz z kolekcji Boeinga</span>
* ''cache''drugiego poziomu (ostatnio także wbudowywana do procesora)
<div class="solution" style="margin-left,margin-right:3em;">
* pamięć operacyjna (RAM): główna pamięć komputera
 
* pamięć zewnętrzna (np. twardy dysk)
Spójrzmy na macierz sztywności dla modelu silnika lotniczego, wygenerowaną swego czasu w zakładach Boeinga i pochodzącą z dyskretyzacji pewnego równania różniczkowego cząstkowego. Pochodzi z [http://www.cise.ufl.edu/research/sparse/matrices/Boeing kolekcji Tima Davisa]. Jest to mała macierz, wymiaru 8032 (w kolekcji spotkasz równania z milionem i więcej niewiadomych).
* pamięć masowa (CD-ROM, taśma streamera, itp.)
 
   
[[Image:MNsparseA.png|thumb|550px|center|Struktura niezerowych elementów macierzy bcsstk38.]]
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
Jej <strong>współczynnik wypełnienia</strong> (to znaczy, stosunek liczby niezerowych do wszystkich elementów macierzy) wynosi jedynie <math>0.006</math>, a więc macierz jest bardzo rozrzedzona: w każdym wierszu są średnio tylko 44 niezerowe elementy.
lokalności w czasie i w przestrzeni:
</div></div>


<blockquote  style="background-color: #fefeee; padding:1em;  margin-left,margin-right:2em;  margin-top,margin-bottom: 1em;"> 
<!--  
* <strong>Lokalność w czasie:</strong> Używać danego fragmentu pamięci intensywnie, ale rzadko.
* <strong>Lokalność w przestrzeni (adresowej):</strong> W danej chwili, odnosić się do adresów pamięci leżących blisko siebie.
</blockquote>


Zachowanie zasady lokalności w czasie jest bardzo ważne dla efektywnego
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
wykorzystania cache'a -- ze względu na małe rozmiary tej pamięci, wymiana
<span  style="font-variant:small-caps;">Przykład: Macierz MBEACXC</span>
zmiennych może być tam intensywna. Zasada lokalności w przestrzeni też jest
<div class="solution" style="margin-left,margin-right:3em;">
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ą?==
Dane pochodzą z serwisu
[http://math.nist.gov/MatrixMarket  MatrixMarket]. Jest to niezbyt duża macierz, wymiaru <math>496 \times 496</math>, niesymetryczna, o elementach rzeczywistych. Źródłem tej macierzy jest pewien model ekonomiczny.


Choć programista nie ma bezpośredniego wpływu na opisane powyżej działania
[[Image:MNmbeacxc|thumb|550px|center|Struktura niezerowych elementów macierzy MBEACXC]]
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 <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.


W programie wykonujemy operację mnożenia dwóch macierzy <math>\displaystyle 1024\times 1024</math> przy
Tylko pozornie macierz ta wydaje się dość gęsta, w rzeczywistości jej współczynnik wypełnienia wynosi około 20 procent.  
użyciu kilku <strong>matematycznie równoważnych</strong> algorytmów (nazwaliśmy je umownie
</div></div>
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:


{| border=1
-->
|+ <span style="font-variant:small-caps"> </span>
   
|-
==Reprezentacja macierzy rzadkich==
|
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
|-
|


|}
Zacznijmy od sposobu przechowywania macierzy rozrzedzonych. Naturalnie, nie ma
sensu przechowywać wszystkich <strong>zerowych</strong> jej elementów: wystarczy
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
wektor, nie będziemy mnożyć przez zera!).


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


'''Algorytm ijk'''
Do zapamiętania macierzy <math>A</math> wymiaru <math>N\times M</math> o <math>NNZ</math> niezerowych elementów wykorzystujemy trzy wektory: <code>I</code>,
<code>J</code> --- oba typu <code>int</code> --- oraz <code>V</code>, typu <code>double</code>,
wszystkie o długości <math>NNZ</math>, przy czym


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<center><math>
A_{I[k], J[k]} = V[k], \qquad\forall k = 0,\ldots, NNZ-1</math></center>
   
   
/* ijk */
[[Image:MNaij-matrix-format.png|thumb|550px|center|Format AIJ]]
for (i = 0; i < N; i++)
 
for (j = 0; j < N; j++)
W tym formacie wprowadzane są macierze rzadkie do Octave'a i MATLABa:
for (k = 0; k < N; k++)
 
C[i*N+j] += A[i*N+k]*B[k*N+j];
<div style="margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><pre>A = sparse(I,J,V,N,N);
</pre></div>
</pre></div>  
   
   
Jest to algorytm, który zapewne większości z Czytelników pierwszy przyszedłby
Jednak wewnętrznie przechowywane są w innym formacie, o którym [[#Format spakowanych kolumn (wierszy)|poniżej]].
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> <strong>oraz</strong> <math>\displaystyle B</math> powodujemy, że przy odwoływaniu się do <math>\displaystyle B</math>,
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
''cache miss'' następuje praktycznie w każdym kroku. Dzieje się tak dlatego,
<span  style="font-variant:small-caps;">Przykład</span>  
że wymiary naszych macierzy są wielokrotnością 512: odwołując się
<div class="solution" style="margin-left,margin-right:3em;">
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'''
Pokażemy jak w Octave wprowadzić macierz rozrzedzoną.


Różni się on od poprzedniego jedynie
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>octave:10> I = [1, 1, 1, 2, 3, 1, 4];
kolejnością dwóch wewnętrznych pętli:
octave:11> J = [1, 3, 2, 2, 3, 4, 4];
octave:12> V = [1, 2, 3, 4, 5, 6, 7];
octave:13> N = 4;
octave:14> A = sparse(I,J,V,N,N)
A =


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Compressed Column Sparse (rows = 4, cols = 4, nnz = 7)
 
  (1, 1) ->  1
  (1, 2) ->  3
  (2, 2) ->  4
  (1, 3) ->  2
  (3, 3) ->  5
  (1, 4) ->  6
  (4, 4) ->  7
 
octave:15> spy(A);
</nowiki></div>
   
   
/* ikj */
Strukturę jej niezerowych elementów ukaże nam polecenie <code style="color: #006">spy(A)</code>. Tak właśnie zostały wygenerowane obrazki macierzy w niniejszym wykładzie.
for (i = 0; i < N; i++)
 
for (k = 0; k < N; k++)
 
for (j = 0; j < N; j++)
</div></div>
C[i*N+j] += A[i*N+k]*B[k*N+j];
 
</pre></div>
====Format spakowanych kolumn (wierszy)====
   
 
Okazuje się, że taka prosta zmiana dramatycznie poprawia sytuację, zmniejszając
Format współrzędnych nie narzucał żadnego uporządkowania elementów macierzy
liczbę ''cache misses''!
--- 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, dobrze 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
przechowywana w postaci trzech wektorów: <code>AV</code> jest wektorem typu
<code>double</code> o długości <math>NNZ</math>, zawierającym <strong>kolejne</strong> niezerowe elementy
macierzy <math>A</math> wpisywane <strong>kolumnami</strong>, <code>AI</code> jest wektorem typu <code>int</code>
o długości <math>NNZ</math>, zawierającym numery wierszy macierzy <math>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>M</math>, zawierający na <math>j</math>-tym miejscu indeks pozycji w <code>AV</code>, od
którego rozpoczynają się w <code>AV</code> elementy <math>j</math>-tej kolumny macierzy <math>A</math>.
 
[[Image:MNcsc-matrix-format.png|thumb|550px|center|Format CSC]]
 
Mamy więc zależność, przy założeniu, że <math>AP[0]=0</math>,
<center><math>
A_{AI[AP[j]+k],j+1} = AV[AP[j]+k], \qquad j = 0,\ldots,M-1, \, k =
0,\ldots,AP[j+1]-1</math></center>
 
Taki (z drobnymi modyfikacjami) format macierzy wykorzystują np. pakiety ARPACK
i UMFPACK (a także, wewnętrznie, Octave i MATLAB).
 
====Format diagonalny====
 
Znacznie mniej uniwersalny niż poprzednie i dlatego rzadziej spotykany. Kolejne
diagonale macierzy przechowujemy w kolejnych wierszach macierzy <math>P\times N</math>,
gdzie <math>P</math> jest liczbą niezerowych diagonal w <math>A\in R^{N\times N}</math>.
 
Szczególnie wygodny do reprezentacji macierzy taśmowych. Wykorzystywany m.in.
przez funkcję LAPACKa <code style="color: #903">DGBSV</code> służącą rozwiązywaniu równań z macierzami
taśmowymi.
 
====Uwagi praktyczne====
 
Mnożenie macierzy w formacie AIJ przez wektor jest kilka razy wolniejsze w porównaniu do macierzy w formacie CSC/CSR (z tych dwóch oczywiście (dlaczego?) CSR jest najszybszy). Co więcej, okazuje się, że w typowych implementacjach, mnożenie macierzy rozrzedzonej (reprezentowanej np. w formacie CSC) przez wektor jest mało efektywne, mniej więcej na poziomie dziesięciu procent możliwości obliczeniowych procesora.
 
Jeśli już ''poważnie'' myśleć o przyspieszeniu mnożenia macierzy przez wektor (np. gdy chcemy stosować [[#Stacjonarne metody iteracyjne|iteracyjną metodę rozwiązywania układu równań]] z tą macierzą), warto rozważyć [http://bebop.cs.berkeley.edu/pubs/vuduc2003-dissertation.pdf  inne formaty] --- łączące w sobie podział macierzy na bloki (tak, jak w algorytmach BLAS) i przechowywanie (w zasadzie) tylko niezerowych elementów macierzy.
 
==Macierze specjalne==


'''Algorytm bikj()'''
Zajmiemy się teraz zadaniem rozwiązywania układu równań liniowych
<center><math>
Ax = b</math>,</center>


Algorytm bikj(16) jest prostą wersją algorytmu blokowego operującego w sposób
ale w sytuacji, gdy macierz <math>A</math> jest rozrzedzona i dużego wymiaru. Dokonamy przeglądu kilku rodzajów algorytmów mających na celu wykorzystanie rozrzedzenia macierzy dla obniżenia kosztu wyznaczenia rozwiązania układu.
"ikj" na blokach macierzy wymiaru <math>\displaystyle 16\times 16</math>:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Należy pamiętać, że z reguły najlepsze wyniki uzyskuje się, gdy konkretny
algorytm dobierze się do konkretnej macierzy. W zastosowaniach pojawiają się
/* bikj(16) */
m.in. macierze rzadkie o bardzo szczególnej strukturze, dla nich warto stosować
for (i = 0; i < N; i+=16)
wyspecjalizowane algorytmy.
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];


</pre></div>
====Macierze taśmowe====
   
(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
Macierz <math>A</math> taka, że dla <math>|i-j| \geq d</math>, <math>a_{ij} = 0</math>, nazywamy macierzą taśmową
wszystkich <math>\displaystyle N^2</math> wartości <math>\displaystyle C</math> i <math>\displaystyle B</math>, przecząc zasadzie <strong>lokalności  w
z rozstępem <math>d</math>, o szerokości pasma <math>2d+1</math>.  
czasie</strong>. Wydzielenie w algorytmie bijk(16) operacji na blokach macierzy ma na celu uniknięcie tego
problemu.


'''Algorytmy DGEMM i ATLAS DGEMM'''
Łatwo sprawdzić, że algorytm eliminacji Gaussa (bez wyboru elementu głównego)
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
oszacowanie rozstępu macierzy rozkładu LU jest równe <math>\max\{2d,N\}</math> --- tak
więc, dla niezbyt dużych <math>d</math> wciąż wynikowa macierz jest taśmowa. 


Algorytm DGEMM to był algorytm mnożenia macierzy z pakietu BLAS --- jest to
W szczególności, gdy macierz jest taśmowa z pasmem o rozstępie <math>d</math> i jednocześnie
właśnie profesjonalny algorytm blokowy, ale niezoptymalizowany na naszą
diagonalnie dominująca, wtedy rozkład LU takiej macierzy da się wykonać w
architekturę. Dopiero algorytm DGEMM podrasowany w pakiecie ATLAS dał nam
miejscu kosztem <math>O(d\,N)</math>, czyli liniowym.
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.


==Reprezentacja macierzy gęstych==
W [http://www.netlib.org/lapack  LAPACKu] zaimplementowano szybki ''solver'' równań z macierzami taśmowymi,
<code style="color: #903">DGBSV</code>, ale wymagający specjalnego sposobu
przechowywania macierzy, wykorzystującego format diagonalny.


Do implementacji algorytmów numerycznych powszechnie używa się dwóch języków:
====Macierze trójdiagonalne====
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.


<strong>W Fortranie, elementy macierzy są przechowywane w pamięci kolumnami</strong>, tzn. jeśli
Szczególnym przypadkiem macierzy taśmowych macierze trójdiagonalne, tzn. taśmowe o
mamy do czynienia z macierzą prostokątną <math>\displaystyle n\times m</math> o elementach <math>\displaystyle a_{ij}</math>,
rozstępie <math>d=1</math>:
<math>\displaystyle i=1\ldots n</math>, <math>\displaystyle j=1\ldots m</math>,


<center><math>\displaystyle
<center><math>
A =
\begin{pmatrix}  
\begin{pmatrix}  
a_{11} & \cdots & a_{1m}\\
a_1 & c_1 &  &  & \\
\vdots &       & \vdots\\
b_2 & a_2 & c_2      &  & \\
a_{n1} & \cdots & a_{nm}
  &  b_3 & a_3  & \ddots &  \\
\end{pmatrix} .
  & & \ddots & \ddots  & c_{N-1}\\
  &  &      & b_N  & a_N
\end{pmatrix}  
</math></center>
</math></center>


to  kolejne miejsca w przestrzeni adresowej
Zadanie rozwiązywania równań z taką macierzą
zajmują elementy
<center><math>
<center><math>\displaystyle a_{11}, a_{21},\ldots, a_{n1}, a_{12}, a_{22}, \ldots, a_{n2},
Ax = e
\ldots a_{nm}.
</math></center>
</math></center>
jest bardzo  często spotykane, więc warto przytoczyć algorytm w całej okazałości --- popularnie zwany <strong>algorytmem przeganiania</strong> (niektóre źródła nazywają go ''algorytmem Thomasa'').
Zacznijmy jednak od stwierdzenia, kiedy macierz trójdiagonalna  nie wymaga wyboru elementu głównego:
{{stwierdzenie|||
Jeśli macierz <math>A</math> ma <strong>słabo dominującą diagonalę</strong>, tzn.
<center><math>|a_i|\,\ge\,|b_i|\,+\,|c_i|,\qquad 1\le i\le N</math>,</center>
(<math>b_1=0=c_N</math>) i przynajmniej dla jednego indeksu "<math>i</math>" mamy powyżej
ostrą nierówność "<math>></math>", to algorytm przeganiania jest wykonalny bez
przestawień wierszy. Ponadto wymaga on <math>7N</math> operacji arytmetycznych,
a więc jest prawie optymalny.
}}
{{algorytm|Metoda przeganiania|Metoda przeganiania|
<pre><math>d_1</math> = <math>a_1</math>;
<math>f_1</math> = <math>e_1</math>;
for (i = 2; i <= N; i++)
{
<math>l</math> = <math>b_i</math>/<math>a_{i-1}</math>;
<math>d_i</math> = <math>a_i</math> - <math>l</math> * <math>c_{i-1}</math>;
<math>f_i</math> = <math>e_i</math> - <math>l</math> * <math>f_{i-1}</math>;
}
<math>x_1</math> = <math>f_N</math>;
for (i = N-1; i >= 1; i--)
<math>x_i</math> = <math>f_i</math> - <math>c_i</math> * <math>x_{i+1}</math>;
</pre>}}
==Metody bezpośrednie==
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Przykład: Strzałka Wilkinsona</span>
<div class="solution" style="margin-left,margin-right:3em;">
Rozważmy układ równań z macierzą diagonalnie dominującą postaci
<center><math>A = \begin{pmatrix}
* & * & * & \cdots & * \\
* & * &        &  & \\
* &  & *      &  &  \\
\vdots & & & \ddots  & \\
* &  &      &  & *
\end{pmatrix}
</math></center>
gdzie <math>*</math> oznacza jakiś niezerowy element.
Łatwo sprawdzić, że chociaż wyjściowa macierz jest rozrzedzona, to zastosowanie do niej eliminacji Gaussa powoduje, że w wyniku
dostajemy <strong>gęste</strong> czynniki rozkładu.
Tymczasem wystarczy odwrócić kolejność równań i numerację niewiadomych (co dla
macierzy jest równoznaczne z odwróceniem porządku wierszy i kolumn, korzystając
z pewnej (jakiej?) macierzy permutacji <math>P</math>):
<center><math>\widetilde{A} = PAP^T = \begin{pmatrix}
* &        &  & & *\\
  &  \ddots  & & & \vdots\\
  &      & * &  & * \\
    &        &  &  * & * \\
* & \cdots & * & * & *
\end{pmatrix}
</math></center>
Wtedy okazuje się, że rozkład naszej macierzy nie powoduje już ''wypełnienia'' czynników rozkładu!
</div></div>
Właśnie na tym polega główny problem w rozwiązywaniu układów z macierzami
rzadkimi metodami bezpośrednimi: jak maksymalnie wykorzystać rozrzedzenie macierzy tak, by czynniki rozkładu były możliwie mało wypełnione. Albowiem wiedząc to będziemy mogli ograniczyć się jedynie do fizycznego wyznaczenia wartości <strong>niezerowych</strong> elementów macierzy rozkładu. Ponadto wymagania pamięciowe algorytmu nie będą istotnie wykraczać ponad ilość pamięci potrzebnej na przechowanie danych (tzn. macierzy).
W ogólnym przypadku rozwiązanie takiego zadania jest trudne i większość
algorytmów opiera się na pewnych heurystykach, które jednak w praktyce warto wspomóc wcześniejszą analizą konkretnego układu równań jaki mamy rozwiązać. Najczęściej dąży się do takiego przenumerowania równań i niewiadomych, by w efekcie z góry przewidzieć, 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 BLAS Level 3, a więc permutacje wierszy i kolumn macierzy muszą to także brać pod uwagę.
Stosuje się kilka strategii wyznaczania korzystnych permutacji
(''reorderingu''), z których warto wymienić
* przybliżone algorytmy minimalnego stopnia (''approximate minimum degree, AMD''), np. [http://www.cise.ufl.edu/research/sparse/amd  AMD];
* techniki podziału grafów na (prawie) rozłączne składowe ''nested dissection, ND'', np. [http://glaros.dtc.umn.edu/gkhome/views/metis  METIS].
   
   
Dla odmiany, <strong>C przechowuje w pamięci elementy macierzy wierszami</strong>, tzn. kolejno
Używają ich biblioteki takie jak [http://www.cise.ufl.edu/research/sparse/umfpack  UMFPACK], czy [http://www.cse.clrc.ac.uk/nag/hsl  HSL].
<center><math>\displaystyle a_{11}, a_{12},\ldots, a_{1m}, a_{21}, a_{22}, \ldots, a_{2m}, \ldots
 
a_{nm}.</math></center>
W Octave mamy do dyspozycji także kilka procedur generujących takie permutacje, w tym: <code style="color: #006">colamd</code> (AMD dla macierzy niesymetrycznych) oraz <code style="color: #006">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, podczas gdy standardowe podejścia nie dadzą efektu.


Co więcej, standard <strong>nie gwarantuje, że kolejne wiersze macierzy będą
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
przechowywane w przylegających do siebie obszarach pamięci</strong>. Bierze się to stąd,
<span  style="font-variant:small-caps;">Przykład</span>  
że w C macierz dwuwymiarowa jest w istocie tablicą wskaźników do kolejnych
<div class="solution" style="margin-left,margin-right:3em;">
wierszy.


To zaś powoduje kolejne komplikacje. Otóż w procedurach numerycznych bardzo
Najprostszy sposób na wykorzystanie metody bezpośredniego rozwiązywania układu z macierzą rzadką to zastosowanie znanego nam operatora do macierzy typu rozrzedzonego:
często pragniemy  dynamicznie przydzielać pamięć na tablice, to znaczy dopiero
 
w trakcie działania procedury, gdyż dopiero w trakcie obliczeń procedura
<div style="margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><pre>A = sparse(...);
otrzymuje np. informację o rozmiarze potrzebnej macierzy. Przykładowo, program w C,
x = A \ b;
który dynamicznie przydzielałby pamięć na dwuwymiarową tablicę,
</pre></div>
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ę dwuwymiarowymi macierzami w C, będziemy stosowali pewien prosty ''trick''.  
Octave domyślnie przyłoży do <math>A</math> reordering <code style="color: #006">colamd</code> i następnie skorzysta z biblioteki UMFPACK by rozwiązać taki układ. Dodatkowo, badane jest wcześniej, czy macierz nie jest przypadkiem taśmowa o wąskiej wstędze: jeśli jest, to korzysta się z LAPACKa.
 
(Jak widzisz, operator rozwiązywania układu równań jest mocno przeciążony: działa i na macierzach kwadratowych, i na prostokątnych, i na rozrzedzonych --- na każdym rodzaju w inny sposób. Nie dziwi Cię chyba, dlaczego wygodnie było napisać Octave w C++?)
 
</div></div>
 
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Przykład: Wypełnienie pewnej macierzy w zależności od użytego reorderingu</span>
<div class="solution" style="margin-left,margin-right:3em;">
 
Rozważmy ponownie macierz pochodzącą z kolekcji [http://www.cise.ufl.edu/research/sparse/matrices/Boeing  Tima Davisa]. Jest to macierz symetryczna, dodatnio określona, wymiaru 8032, o 355460 elementach niezerowych i, w konsekwencji, o wypełnieniu około 0.6 procent.
 
[[Image:MNsparseA.png|thumb|550px|center|Struktura niezerowych elementów macierzy.]]
 
Zobaczymy, jak w zależności od użytego algorytmu permutacji kolumn i
wierszy poradzi sobie algorytm rozkładu Cholesky'ego.
 
[[Image:MNsparsechol.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> wykonanego
standardowym algorytmem. Czas rozkładu: 0.892013]]
 
[[Image:MNsparsecholcolamd.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z reorderingiem
COLAMD. Czas rozkładu: 0.813038]]
 
[[Image:MNsparsecholsymamd.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z reorderingiem
SYMAMD. Czas rozkładu: 0.487683s. Prawie dwa razy
szybciej niż bez reorderingu, chociaż i tak wskutek wzrostu wypełnienia macierzy
w dolnym trójkącie mamy aż 2 procent niezerowych elementów.]]
 
[[Image:MNsparsecholsymrcm.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z odwrotnym reorderingiem
Cuthill-McKee. Czas rozkładu: 0.845928]]
 
[[Image:MNsparsecholcolperm.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z jeszcze
innym (bardzo tanim, ale jak widać czasem zupełnie złym) reorderingiem. Czas rozkładu: 5.947936]]
 
Na zakończenie popatrzmy, jak ważne jest spostrzeżenie symetrii macierzy:
 
[[Image:MNsparseLlu.png|thumb|550px|center|Rozkład LU, czynnik <math>L</math> (bez reorderingu). Czas rozkładu LU: 1.696018s. Nieakceptowalny, podobnie
jak drastyczne wypełnienie macierzy.]]
 
[[Image:MNsparseUlu.png|thumb|550px|center|Rozkład LU, czynnik <math>U</math> (bez reorderingu)]]
 
Jak widać, w naszym przypadku standardowe algorytmy (COLAMD i SYMAMD) poradziły sobie całkiem nieźle, chociaż wypełnienie i tak znacząco wzrosło. Zapewne, dla tej macierzy, która jest specyficznego typu --- pochodzi z dyskretyzacji równania różniczkowego --- algorytm ND mógłby tu jeszcze lepiej zadziałać.
 
</div></div>


Dlatego przyjmiemy konwencję, że nie będziemy wcale korzystać z macierzy
==Stacjonarne metody iteracyjne==
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,
Gdy macierz jest rozrzedzona, mnożenie takiej macierzy przez wektor jest bardzo tanie (koszt jest proporcjonalny do liczby niezerowych elementów macierzy).
wybierzemy zazwyczaj układ kolumnowy (czyli fortranowski) (pamiętajmy wszak, że
Dlatego, jeśli możemy zadowolić się <strong>rozwiązaniem przybliżonym</strong> układu, a w zamian osiągnąć je tanim kosztem, warto rozważyć metody iteracyjne.
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 pokazujemy przykładowy fragment kodu realizującego opisaną wyżej ideę.
Najprostsze metody iteracyjne (najprostsze w analizie i implementacji, ale ---
jak można się domyślić --- w praktyce najmniej efektywne) polegają na rozkładzie
macierzy na część "łatwo odwracalną", <math>M</math>, i "resztę", <math>Z</math>. Dokładniej,
jeśli <math>M</math> jest nieosobliwa, to równanie <math>Ax = b</math> można zapisać jako
zadanie punktu stałego


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<center><math>Mx = Nx + b</math>,</center>
#define N 10
#define IJ(i,j,n) ((i)-1+((j)-1)*(n))
#include <stdio.h>


int main(void)
gdzie <math>Z=M-A</math>. Inaczej:
{
 
double *matrix, *ptr;
<center><math>x = M^{-1}(Zx + b)</math>,</center>
int i,j;
 
i zastosować doń [[MN02#Iteracja prosta Banacha|metodę iteracji prostej Banacha]]:
 
<center><math>x_{k+1} = M^{-1}(Zx_k + b)</math></center>
 
Takie metody nazywamy stacjonarnymi metodami iteracyjnymi. Aby przeanalizować zbieżność takiej metody, warto rozpatrzyć przypadek ogólniejszy


matrix = (double *)malloc(N*N*sizeof(double));
<center><math>
  x_k\,=\,B x_{k-1}\,+\, c</math>,</center>


ptr = matrix;
dla pewnej macierzy <math>B</math>  oraz wektora <math>c</math>. (Dla stacjonarnej metody iteracyjnej, <math>B=M^{-1}Z</math> oraz <math>c=M^{-1}b</math>.)


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


for (j=1; j<=N; j++)
<center><math>x_k- x^*\,=\,B^k( x_0- x^*),
for (i=1; i<=N; i++)
</math></center>
{
*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++)
a stąd i z nierówności <math>\|B^k\|\le\|B\|^k</math>, mamy
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");
}


free(matrix);
<center><math>\| x_k- x^*\|\,\le\,
return(0);
          \|B\|^k\| x_0- x^*\|.  
}
</math></center>
</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.


Za drugim razem chcemy wydrukować naszą macierz na ekranie, musimy więc
Warunkiem dostatecznym zbieżności iteracji prostych
odwoływać się do kolejnych <strong>wierszy</strong> macierzy (a więc, z punktu
jest więc <math>\|B\|<1</math>. Okazuje się, że warunkiem koniecznym i dostatecznym
wykorzystania cache i hierarchii pamięci, fatalnie! -- na szczęście I/O jest
zbieżności tej iteracji dla dowolnego wektora startowego <math>x_0</math> 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.


==Włączenie zewnętrznej biblioteki fortranowskiej do programu==
<center><math>\rho = \max\{ |\lambda| : \lambda  \mbox{ jest wartością własną }  B\} < 1</math></center>


Istnieje prosty sposób wykorzystania gotowych pakietów
Tak więc, metody oparte na iteracji prostej będą zbieżne liniowo z ilorazem
fortranowskich przez zewnętrzne programy w C: wystarczy skorzystać z genialnej
<math>\rho</math>.
biblioteki <code style="color: #666">f2c</code> lub jej modyfikacji na użytek kompilatora GCC,
biblioteki <code style="color: #666">gfortran</code>.


Napiszemy program, który będzie liczył normę zadanego
Zaletą stacjonarnych metod iteracyjnych jest również ich prostota,  
wektora, korzystając z funkcji <code style="color: #903">DNRM2</code> biblioteki
przez co są one łatwe do zaprogramowania, co łatwo zobaczyć na przykładach metod: [[#Metoda Jacobiego|Jacobiego]] i [[#Metoda Gaussa-Seidela|Gaussa--Seidela]], które teraz omówimy.  
[http://www.netlib.org/blas  BLAS].  


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


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Biorąc <math>M \mbox{diag} (A)</math>, gdzie <math>\mbox{diag} (A)</math> jest macierzą diagonalną składającą się
z wyrazów stojących na głównej przekątnej macierzy
      DOUBLE PRECISION FUNCTION DNRM2 ( N, X, INCX )
<math>A</math>, układ <math>A x= b</math> jest równoważny układowi
*    .. 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>
Nasza funkcja obliczająca normę wektora ma więc trzy argumenty: <code style="color: #903">N</code> --
długość wektora (<code style="color: #903">INTEGER</code>), <code style="color: #903">X</code> -- wektor, którego długość chcemy
obliczyć (tablica liczb <code style="color: #903">DOUBLE PRECISION</code>) oraz tajemniczy dodatkowy parametr
<code style="color: #903">INCX</code> typu <code style="color: #903">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 style="color: #903">INCX</code> równe 1. Używając zapisu Octave, <code style="color: #903">DNRM2</code>
oblicza po prostu


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<center><math>M x = Z x + b</math>,</center>
norm( X(1:INCX:N) )
</pre></div>
   
Kod obiektowy tej funkcji znajduje się już w bibliotece BLAS zawartej w pliku
<code style="color: #666">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?


Zacznijmy od nazwy. W przypadku kompilatora
a stąd (o ile na przekątnej macierzy <math>A</math> nie mamy zera)  
<code style="color: #666">gcc</code>/<code style="color: #666">gfortran</code>, nazwą funkcji do wykorzystania w C będzie
otrzymujemy metodę iteracyjną
<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
<center><math>x_k\,=\,B x_{k-1}\,+\, c,
wątpliwości: jako wektor <code style="color: #903">X</code> przekażemy -- naturalnie -- <strong>wskaźnik</strong> do
</math></center>
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:


{| border=1
gdzie <math>B=-M^{-1}Z</math> i <math>c=M^{-1} b</math>, zwaną
|+ <span style="font-variant:small-caps"> </span>
<strong>metodą Jacobiego</strong>.
|-
|
Fortran 77  ||  C
|-
|
<code style="color: #903">INTEGER</code>  ||  <code>int</code>
|-
| <code style="color: #903">REAL</code>  ||  <code>float</code>
|-
| <code style="color: #903">DOUBLE PRECISION</code>  ||  <code>double</code>
|-
| <code style="color: #903">COMPLEX</code>  ||  <code>struct { float Re, Im; }</code>
|-
| <code style="color: #903">DOUBLE COMPLEX</code>  ||  <code>struct { double Re, Im; }</code>
|-
| <code style="color: #903">CHARACTER</code>  ||  <code>char</code>
|-
|


|}
Rozpisując ją po współrzędnych dostajemy (numer iteracji wyjątkowo zaznaczamy w postaci ''górnego'' indeksu) układ rozszczepionych równań:


A więc pierwszym i trzecim argumentem funkcji <code>dnrm2_</code>
<center><math>x^{(k)}_i = \frac{1}{a_{ii}} \left( b_i - \sum_{j\neq i} a_{ij}x^{(k-1)}_j \right)</math>,</center>
będą wskaźniki do <code>int</code>. Ponieważ
funkcja <code style="color: #903">DNRM2</code> zwraca w wyniku liczbę podwójnej precyzji, to ostatecznie
prototyp naszej funkcji w C będzie następujący:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
co znaczy dokładnie tyle, że w <math>i</math>-tym równaniu wyjściowego układu przyjmujemy za współrzędne <math>x</math> wartości z poprzedniej iteracji i na tej podstawie wyznaczamy wartość <math>x_i</math>.
double dnrm2_(int* N, double* X, int* INCX);
</pre></div>
No to wykorzystajmy naszą funkcję:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
{{twierdzenie|O zbieżności metody Jacobiego|O zbieżności metody Jacobiego|
/* Wykorzystanie funkcji DNRM2 fortranowskiej biblioteki BLAS w programie w C*/
#include <stdio.h>
double dnrm2_(int*,double*,int*);


int main(void)
W metodzie Jacobiego warunek dostateczny zbieżności,
{
<math>\|B\|<1</math>, jest spełniony np. wtedy, gdy macierz <math>A</math> ma
int n, incx=1;
dominującą przekątną, tzn. gdy
double x[3]= {0,1,2};
n = 3;
printf("Norma podanego wektora: &#37;e\n", dnrm2_(&n, x, &incx));


return(0);
<center><math>
}
  |a_{i,i}|\,>\,\sum_{j\neq i}|a_{i,j}|,\qquad \forall i = 1,\ldots, N</math></center>
</pre></div>
Zwróćmy uwagę na sposób kompilacji tego programu:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
}}
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 style="color: #666">gfortran</code>, umożliwiającą koegzystencję Fortranu i
C.


====Funkcja fortranowska z argumentem macierzowym====
{{dowod|||
Rzeczywiście, ponieważ wyraz <math>(i,j)</math> macierzy <math>M^{-1}Z</math>
wynosi <math>0</math> dla <math>i=j</math> oraz <math>a_{i,j}/a_{i,i}</math> dla <math>i\neq j</math>, a więc


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<center><math>\begin{align} \lefteqn{\|M^{-1}Z\|_\infty \;=\; \max_{1\le i\le N}
  \sum_{j=1,j\ne i}^N{|a_{i,j}|}/{|a_{i,i}|} } \\
test funkcji
    && =\;\max_{1\le i\le N}
</pre></div>
      \sum_{j=1}^N{|a_{i,j}|}/{|a_{i,i}|}\,-\,1\;<\;1,
\end{align}</math></center>


Należy szczególną uwagę zwrócić na argumenty macierzowe funkcji w Fortranie,
przy czym ostatnia nierówność wynika z warunku diagonalnej dominacji.  
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>:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
   
<span style="font-variant:small-caps;">Przykład: Macierz laplasjanu</span>  
SUBROUTINE FILLMATRIX ( M, N, MATRIX )
<div class="solution" style="margin-left,margin-right:3em;">
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
Macierz <math>N\times N</math>, zwana <strong>macierzą jednowymiarowego laplasjanu</strong>
\lstF{MATRIX} =  
<center><math>
\begin{pmatrix}  
L = \begin{pmatrix}  
3 & 5 & 7 & 9 & 11 \\
2 & -1 &      & \\
4 & 6 & 8 & 10 & 12
-1 & 2 & \ddots & \\
    & \ddots & \ddots & -1 \\
    &     & -1 & 2
\end{pmatrix}  
\end{pmatrix}  
</math></center>
</math></center>


Naturalnie, możemy wywołać ją wprost z programu w C przy użyciu poprzednio
pojawia się w bardzo wielu zastosowaniach, także jako podzadanie w algorytmach
poznanej techniki i następującego kodu (tym razem prototyp funkcji
numerycznych. Ta macierz jest macierzą taśmową, symetryczną i dodatnio
<code>fillmatrix_</code> umieszczamy w osobnym pliku nagłówkowym <code style="color: #666">ffortran.h</code>,
określoną,  więc układ równań z tą macierzą można bez trudu rozwiązać metodami
gdzie mamy zamiar kolekcjonować prototypy w C lokalnie wykorzystywanych funkcji
bezpośrednimi, kosztem <math>O(N)</math>. Stosując do niej metodę Jacobiego mamy <math>M = 2I</math> oraz <math>Z = L - M</math>. Obliczając
fortranowskich):
normę macierzy iteracji Jacobiego dostajemy <math>||M^{-1}Z||_\infty
= 1</math>, co nie rozstrzyga jeszcze o jej zbieżności lub niezbieżności.


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Potrzebna będzie bardziej subtelna analiza, Okazuje się, że są znane wzory na wartości własne macierzy <math>L</math>:
/*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()
<center><math>\lambda_j = 4\sin^2 \left(\frac{j \pi }{2(N+1)} \right)</math>,</center>
{
int MM, NN, i, j;
double *A;
  MM = 2; NN = 5;
A = (double *)malloc(MM*NN*sizeof(double));


  fillmatrix_( &MM, &NN, A );
dla <math>1 \leq j \leq N</math>.


printf("\nKolejne elementy wektora A:\n\n"); 
W konsekwencji, wartościami własnymi <math>M^{-1}Z = \frac{1}{2}Z = \frac{1}{2}L -
  for ( i = 0; i < NN*MM ; i++ ){
I</math> są liczby <math>\mu_i = \frac{1}{2} \lambda_i - 1</math>. Ponieważ <math>0 < \mu_i < 1</math>, znaczy to, że metoda Jacobiego jest zbieżna dla macierzy <math>L</math>.
printf("&#37;e\n", A[i] );
}


printf("\nWektor A zinterpretowany jako macierz:\n\n")
Z drugiej strony, nie dajmy się zwieść optymizmowi matematyka (''"nie martw się, jest zbieżny..."''): nietrudno sprawdzić, że <math>||Z||_2 = 1 - O(N^{-2}) < 1</math>, co oznacza, że metoda Jacobiego --- choć zbieżna --- dla dużych <math>N</math> staje się
for ( j = 0 ; j < MM ; j++ )
zbieżna tak wolno, że w praktyce bezużyteczna!
{
</div></div>
  for ( i = 0; i < NN ; i++ )
  printf("&#37;e ", A[i*MM+j] );
printf("\n");
  }


  free( A );
====Metoda Gaussa-Seidela====


  return(0);
<!--
}
Ciekawostką jest, że Gauss nie miał z nią nic wspólnego, a Seidel był podobno jej przeciwnikiem...
</pre></div>
-->
   
   
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ę
Heurystyka tej metody opiera się na zmodyfikowaniu metody Jacobiego tak, by w każdym momencie iteracji korzystać z najbardziej "aktualnych" współrzędnych przybliżenia rozwiązania <math>x</math>.
jednowymiarową</strong> odpowiedniej wielkości.


==BLAS, LAPACK i ATLAS==
Rzeczywiście, przecież rozwiązując równanie metody Jacobiego:


W zadaniach dotyczących macierzy gęstych, warto skorzystać z klasycznego tandemu
<center><math>x^{(k)}_i = \frac{1}{a_{ii}} \left( b_i - \sum_{j\neq i} a_{ij}x^{(k-1)}_j \right)</math>,</center>
bibliotek: [http://www.netlib.org/blas  BLAS] ''Basic Linear Algebra Subprograms''
oraz [http://www.netlib.org/blas  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>.


[[Image:MNdegsvtiminglogscale.png|thumb|550px|center|Porównanie czasu działania kodu w C, implementującego algorytm rozkładu LU z wykładu, z czasem działania procedury <code style="color: #903">DGESV</code> z LAPACKa, niezoptymalizowanej i zoptymalizowanej (ATLAS) na daną architekturę. Zwróć uwagę na to, że skala jest logarytmiczna!]]
nietrudno zauważyć, że w części sumy moglibyśmy odwoływać się do "dokładniejszych" wartości <math>x^{(k)}_j</math>: dla <math>j < i</math>, tzn.


BLAS i LAPACK są często wykorzystywane przez inne biblioteki numeryczne,
<center><math>x^{(k)}_i = \frac{1}{a_{ii}} \left( b_i - \sum_{j< i} a_{ij}x^{(k)}_j - \sum_{j>i} a_{ij}x^{(k-1)}_j \right)</math></center>
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 [http://www.intel.com/cd/software/products/asmo-na/eng/perflib/mkl  MKL]) oraz AMD (biblioteka
[http://developer.amd.com/acml.aspx  ACML])


BLAS jest kolekcją procedur służących manipulacji podstawowymi obiektami
W języku rozkładu macierzy <math>A = M - Z</math> i iteracji <math>x_{k+1} = M^{-1}(Zx_k + b)</math> mamy więc <math>M =  \mbox{tril} (A)</math> (dolny trójkąt macierzy <math>A</math>).
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 <strong>wektor--wektor</strong>, 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 <strong>macierz--wektor</strong>, 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;
* BLAS Level 3 -- operacje typu <strong>macierz--macierz</strong>, 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ą.
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.


Większość użytkowników BLAS nie będzie jednak miała potrzeby pisania własnych
{{twierdzenie|O zbieżności metody Gaussa-Seidela|O zbieżności metody Gaussa-Seidela|
algorytmów blokowych, gdyż funkcje rozwiązujące podstawowe zadania numerycznej
algebry liniowej --- m.in. rozwiązywanie układów równań (także nad- i niedookreślonych oraz zadania własnego --- znajdują się w doskonałej bibliotece LAPACK , która intensywnie i skutecznie wykorzystuje podprogramy BLAS.


Nazwy procedur BLASów i
Jeśli macierz <math>A</math> jest diagonalnie dominująca, to metoda Gaussa--Seidela jest zbieżna dla dowolnego wektora startowego <math>x_0</math>.
LAPACKa są cokolwiek enigmatyczne na pierwszy rzut oka, ale w istocie dość
}}
łatwo je odgadywać. Każda nazwa składa się z kilku części, najczęściej jest
postaci <code style="color: #903">PRRFF</code>, gdzie
;
<code style="color: #903">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;


;
Metoda Gaussa--Seidela jest w wielu przypadkach rzeczywiście szybciej zbieżna od metody Jacobiego, np. tak jest w przypadku macierzy jednowymiarowego Laplasjanu. Wciąż jednak, dodajmy, dla zadań bardzo źle uwarunkowanych jej zbieżność jest zbyt wolna by ją stosować jako samodzielną metodę.
:  <code style="color: #903">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;


;
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<code style="color: #903">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)
<span  style="font-variant:small-caps;">Uwaga</span>  
<div class="solution" style="margin-left,margin-right:3em;">
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
[http://www.netlib.org  Netlib].


Zestawienie najczęściej wykorzystywanych procedur BLAS i LAPACKa przedstawiamy
Obie metody, Jacobiego i (zwłaszcza) Gaussa--Seidela stosuje się także czasem w prostych algorytmach rozwiązywania <strong>układów równań nieliniowych</strong>: ich zaletą jest to, że głównym składnikiem iteracji jest rozwiązywanie [[MN02|<strong>skalarnego</strong> równania nieliniowego]] na każdym kroku metody.
poniżej. Każda z tych procedur ma swój wariant "ekspercki", np. dla
</div></div>
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
====Złożoność stacjonarnych metod iteracyjnych====
|+ <span style="font-variant:small-caps"> </span>
|-
|


Zadanie algebry liniowej || Nazwa procedury BLAS/LAPACK
Zastanówmy się teraz nad złożonością metod iteracyjnych. Ponieważ możemy jedynie znaleźć pewne przybliżenie rozwiązania dokładnego <math>x^*</math>, przez złożoność metody będziemy rozumieli koszt kombinatoryczny obliczenia <math>x_k</math> z zadaną dokładnością <math>\epsilon>0</math>. Dla uproszczenia założymy, że medoda jest zbieżna liniowo z ilorazem <math>\rho</math>. Zauważmy, że aby zredukować błąd początkowy do <math>\epsilon>0</math>, wystarczy wykonać <math>k=k(\epsilon)</math> iteracji, gdzie <math>k</math> spełnia
|-
|


mnożenie wektora przez macierz  ||  <code style="color: #903">DGEMV</code>
<center><math>\rho^k\| x_0- x^*\|\,\le\,\epsilon</math>,</center>
|-
| mnożenie macierzy przez macierz  ||  <code style="color: #903">DGEMM</code>
|-
|
rozwiązywanie układu równań  ||  <code style="color: #903">DGESV</code>
|-
| rozkład LU (w miejscu)  ||  <code style="color: #903">DGETRF</code>
|-
| rozwiązywanie układu z rozkładem uzyskanym  z <code style="color: #903">DGETRF</code>  ||  <code style="color: #903">DGETRS</code>
|-
|
rozwiązywanie układu z macierzą symetryczną  ||  <code style="color: #903">DSYSV</code>
|-
| rozkład LDL<math>\displaystyle ^T</math> macierzy symetrycznej (w miejscu)  ||  <code style="color: #903">DSYTRF</code>
|-
| rozwiązywanie układu z rozkładem uzyskanym z <code style="color: #903">DSYTRF</code>  ||  <code style="color: #903">DSYTRS</code>
|-
|
rozwiązywanie układu z macierzą pasmową  ||  <code style="color: #903">DGBSV</code>
|-
| rozkład LU macierzy pasmowej (w miejscu)  ||  <code style="color: #903">DGBTRF</code>
|-
| rozwiązywanie układu z rozkładem uzyskanym  z <code style="color: #903">DGBTRF</code> ||  <code style="color: #903">DGBTRS</code>
|-
|
zagadnienie własne  ||  <code style="color: #903">DGEEV</code>
|-
|


|}
czyli


<!--  
<center><math>k\,\ge\,\frac{\log(1/\epsilon) - \log(1/\| x_0- x^*\|)}{\log(1/\rho)}</math></center>


====Mnożenie macierz-wektor w BLAS====
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>\rho</math>, natomiast zależność od dokładności <math>\epsilon</math> i wymiaru <math>N</math> układu jest dużo mniej istotna (w zadaniach praktycznych -- takich jak jednowymiarowy laplasjan --- jednak często
okazuje się, że... <math>\rho</math> zależy od <math>N</math>!).


Zacznijmy od prostej procedury BLAS Level 2, jaką jest mnożenie macierzy przez
Zakładając, że koszt jednej iteracji wynosi <math>c=c(N)</math> (<math>c(N)</math> jest tym mniejszy, im mniejsza jest liczba niezerowych elementów macierzy <math>A</math>), złożoność metody jest proporcjonalna do
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>c(N)\,\frac{\log(1/\epsilon)}{\log(1/\rho)}.
<center><math>\displaystyle
y \leftarrow  \alpha A x + y,
</math></center>
</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>
Stąd oczywisty wniosek (prawdziwy nie tylko dla metod stacjonarnych), że metody iteracyjne warto stosować zamiast metod bezpośrednich w przypadku, gdy
współrzędnych.
* wymiar <math>N</math> układu <math>A x= b</math> jest "duży", oraz
* macierz <math>A</math> układu jest "rozrzedzona", tzn. ma stosunkowo niewielką liczbę elementów  niezerowych, np. proporcjonalną do <math>N</math>.
 
Układy o tych własnościach powstają często przy numerycznym rozwiązywaniu równań różniczkowych cząstkowych.  
 
==Metody przestrzeni Kryłowa==


To zadanie realizuje procedura BLASów o nazwie
Zupełnie inny pomysł na realizację metody iteracyjnej przedstawiają metody przestrzeni Kryłowa, gdzie kolejne przybliżenie <math>x_k</math> dobiera się w taki sposób, by minimalizowało pewną miarę błędu na podprzestrzeni Kryłowa
<code style="color: #903">DGEMV</code>. W rzeczywistości ta procedura wykonuje ogólniejsze zadanie
wyznaczania wektora


<center><math>\displaystyle
<center><math>K_k =  \mbox{span} \{r,A_r,\ldots, A^{k-1}r \}</math>,</center>
y \leftarrow \alpha B x + \beta y,
 
gdzie <math>r = b-Ax_0</math> jest ''residuum'' na początku iteracji. (Zwróć uwagę, że przestrzeń Kryłowa jest rozpięta przez kolejne wektory [[MN13#Metoda potęgowa|metody potęgowej]]] --- to nie przypadek!). W zależności od wyboru sposobu miary błędu, dostajemy inną metodę iteracyjną, takie jak CG, GMRES, PCR, BiCG, i inne. Tutaj omówimy pokrótce tylko najpopularniejszą: CG.
 
====CG====
 
Metoda <strong>gradientów sprzężonych</strong>, w skrócie CG (''conjugate gradients''), działa przy założeniu, że <math>A</math> jest <strong>symetryczna i dodatnio określona</strong>.
 
Kolejne przybliżenie <math>x_k</math> ma minimalizować błąd w normie energetycznej
indukowanej przez <math>A</math>,
 
<center><math>||x_k -x||_A = \sqrt{(x_k -x)^TA(x_k -x)}
</math></center>
</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
na przestrzeni afinicznej <math>x_0 + K_k</math>. Okazuje się (co ''nie jest'' oczywiste --- trzeba skorzystać z rozmaitych własności ortogonalności generowanych wektorów), że takie zadanie minimalizacji daje się bardzo efektywnie rozwiązać, skąd dostajemy bardzo zwarty algorytm:
razem argumentem macierzowym, jaki przekazujemy <code style="color: #903">DGEMV</code>, jest wyjściowa
 
macierz <math>\displaystyle A</math>).
{{algorytm|Metoda CG|Metoda CG|
<pre>
r = b-A*x;
<math>\rho_0</math> = <math>||r||_2^2</math>; <math>\beta</math> = 0; k = 1;
while (!stop)
{
p = r + <math>\beta</math>*p;
w = A*p;
<math>\alpha</math> = <math>\rho_{k-1}</math>/<math>p^Tw</math>;
x = x + <math>\alpha</math>*p;
r = r - <math>\alpha</math>*w;
<math>\rho_k</math> = <math>||r||_2^2</math>;
<math>\beta</math> = <math>\frac{\rho_{k}}{\rho_{k-1}}</math>;
k++;
}
</pre>
}}
 
Jak widać, całą iterację da się wykonać przechowując w pamięci tylko kilka wektorów (a nie, jak możnaby się obawiać, całą przestrzeń <math>K_k</math>), a najdroższym jej elementem jest mnożenie macierzy przez wektor.


Jak wiemy, że jest możliwe bezpośrednie wykorzystanie
{{twierdzenie|O zbieżności CG jako metody bezpośredniej|O zbieżności CG jako metody bezpośredniej|
biblioteki fortranowskiej w programie w C, jednak musimy pamiętać, iż macierze z
jakich ona skorzysta muszą być ułożone <strong>kolumnami</strong> w jednolitym bloku
pamięci.


Bazując na opisie procedury <code style="color: #903">DGEMV</code> ze
Niech <math>A\in R^{N\times N}</math> będzie symetryczna i dodatnio określona. Algorytm CG znajdzie dokładne rozwiązanie po co najwyżej <math>N</math> iteracjach.
strony \pageref{opis:dgemv}, w programie w C powinniśmy
}}
napisać prototyp tej funkcji następująco:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Powyższe twierdzenie, choć teoretycznie interesujące, ma małą wartość praktyczną z dwóch powodów:
* dla bardzo dużych <math>N</math>, wykonanie <math>N</math> iteracji może być wciąż zbyt kosztownym zadaniem;
* ponieważ w arytmetyce skończonej precyzji ortogonalność --- z której w bardzo istotny sposób korzysta się przy wyprowadzeniu algorytmu --- pogarsza się z iteracji na iterację i w konsekwencji, po wielu iteracjach, jakość <math>x_k</math> przestaje się poprawiać.
   
   
int dgemv_( char* TRANS,
Dlatego wygodniej potraktować CG jako metodę iteracyjną. Zachodzi bowiem
int* M,
 
int* N,
{{twierdzenie|Zbieżność CG jako metody iteracyjnej|Zbieżność CG jako metody iteracyjnej|
double* ALPHA,
 
double* A, 
Po <math>k</math> iteracjach metody CG,  
int* LDA, 
 
double* X, 
<center><math>||x_k - x||_A \leq 2 \, \left( \frac{\sqrt{ \mbox{cond} (A)} - 1}{\sqrt{ \mbox{cond} (A)} +1}\right)^k \, ||x_0 - x||_A</math>,</center>
int* INCX,
 
double* BETA,
gdzie <math>\mbox{cond} (A) = ||A||_2||A^{-1}||_2</math>.
double* Y,  
}}
int* INCY );
 
</pre></div>
====GMRES====
 
 
Dla własnej wygody, a także dla przyszłego wykorzystania, umieścimy ten
Metoda GMRES (''Generalized Minimum RESidual'') nie wymaga ani symetrii, ani dodatniej określoności macierzy, jest więc bardziej uniwersalna, choć też bardziej kosztowna od CG. Jej szczegółowe omówienie, w tym --- oszacowania szybkości zbieżności --- wykracza niestety poza ramy niniejszego wykładu.
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 = 1, BLASMONE = -1;</code>), w pliku
nagłówkowym <code style="color: #666">blaslapack.h</code>.


Dalej już jest łatwo: oto pełny kod programu realizującego operację mnożenia macierzy przez wektor
====Ściskanie macierzy====
przy użyciu procedury BLAS <code style="color: #903">DGEMV</code>:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Zbieżność wszystkich poznanych metod iteracyjnych zależy od własności
#include <stdio.h>
spektralnych macierzy układu. Pojawiające się w zastosowaniach macierze
#include "blaslapack.h"
często mają niekorzystne własności spektralne (np. bardzo duży wskaźnik uwarunkowania), przez co metody iteracyjne zbiegają na nich bardzo wolno.


double* mmread(char *filename, int* N, int* M );
Dlatego bardzo korzystne może być wstępne przetransformowanie układu


int main()
<center><math>Ax = b
{
</math></center>
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;


/* obliczamy y = 5*A*x, korzystając z procedury BLAS L2: DGEMV */
z macierzą o niekorzystnych własnościach, do układu


{
<center><math>MAx = Mb</math>,</center>
char TRANS = 'N'; double ALPHA = 5.0, BETA = 0.0;
dgemv_(&TRANS, &N, &M, &ALPHA, A,  &N,  x,  &BLASONE,
                        &BETA, y, &BLASONE );


}
gdzie macierz <math>MA</math> ma znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej. W przypadku macierzy symetrycznych widzieliśmy, że kluczowe znaczenie dla zbieżności metody miał rozkład wartości własnych: jeśli wartości własne były bardzo rozrzucone po prostej, to uwarunkowanie było bardzo duże i w konsekwencji zbieżność powolna. Aby zbieżność była szybsza, kluczowe jest, by:
* wartości własne <math>MA</math> były upakowane w klastrach
/* wydruk wyniku */
* najlepiej wszystkie w (małym) otoczeniu wartości 1
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,
Jeśli więc chcielibyśmy przekształcić macierz tak, by metoda iteracyjna dla <math>MA</math> zbiegała szybko, musimy w jakiś sposób "ścisnąć" spektrum macierzy <math>A</math> w okolice jedności. Taką operację nazywamy <strong>ściskaniem</strong> (''preconditioning''), a macierz <math>M</math> --- <strong>imadłem</strong>.
predefiniowanej w pliku <code style="color: #666">blaslapack.h</code>.  


-->
Aby całość miała sens, macierz ściskająca <math>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>M\cdot (A \cdot x)</math>),
* macierz <math>MA</math> powinna mieć znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej.
   
   
Programy korzystające z BLASów i LAPACKa kompilujemy
Kilka ekstremalnych propozycji na macierz imadła to <math>M = I</math> (łatwa w
standardowo, pamiętając o dołączeniu na etapie linkowania używanych przez nas
konstrukcji i tania w mnożeniu, ale niestety nic nie polepsza...) oraz
bibliotek:
<math>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 (aczkolwiek wciąż nie najbardziej skutecznych) sposobów ściskania są te oparte na zastosowaniu jednego kroku klasycznej metody iteracyjnej.
 
[[Image:MNpcgconv.png|thumb|550px|center|Zbieżność metody CG bez żadnego  ściskania oraz ściśniętej
imadłem opartym na jednej iteracji (blokowej) metody Jacobiego.]]
 
Inne sposoby ściśnięcia macierzy wykorzystują np. techniki tzw. <strong>niepełnego
rozkładu</strong> macierzy, albo --- w specyficznych przypadkach --- tzw.<strong> metody
wielosiatkowe</strong>.
 
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 imadła.
 
==Literatura==


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
W celu dogłębnego zapoznania się z omawianym na wykładzie materiałem, przeczytaj <b>rozdział 4.6 i 4.7</b> w
* D. Kincaid, W. Cheney <cite>Analiza numeryczna</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa 2006, ISBN 83-204-3078-X.
   
   
gcc -o testblas testblas.c -llapack -lblas -lgfortran -lm
Tym razem wymienione rozdziały nie wystarczą do dogłębnego zrozumienia omawianych treści. Warto więc sięgnąć po jeden z podręczników
</pre></div>
* <span style="font-variant:small-caps">C. T. Kelley</span>, <cite>Iterative Solution of Systems of Linear and Nonlinear Equations</cite>, SIAM, 1995,
* <span style="font-variant:small-caps">Y. Saad</span>, <cite>[http://www-users.cs.umn.edu/&nbsp;saad/books.html  Iterative Methods for Sparse Linear Systems]</cite>, PWS, 1996.

Aktualna wersja na dzień 22:13, 11 wrz 2023


Wielkie układy równań liniowych

<<< Powrót do strony głównej przedmiotu Metody numeryczne

Ćwiczenia do wykładu >>>


Wstęp

Wraz z coraz większymi modelami pojawiającymi się w praktyce obliczeniowej, coraz częściej zachodzi potrzeba rozwiązywania zadań algebry liniowej, w której macierze są co prawda wielkiego wymiaru, ale najczęściej rozrzedzone, to znaczy jest w nich bardzo dużo zer. Bardzo często zdarza się, że macierz wymiaru N ma tylko O(N) niezerowych elementów. Wykorzytanie tej specyficznej własności macierzy nie tylko prowadzi do algorytmów istotnie szybszych od ich analogów dla macierzy gęstych (to znaczy takich, które (w założeniu) mają N2 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 są np. równania różniczkowe cząstkowe (a więc np. modele pogody, naprężeń w konstrukcji samochodu, przenikania kosmetyków do głębszych warstw skóry, itp.).

Modele wielostanowych systemów kolejkowych (np. routera obsługującego wiele komputerów) także prowadzą do gigantycznych układów równań z macierzami rozrzedzonymi o specyficznej strukturze.

Z reguły zadania liniowe wielkiego wymiaru będą miały strukturę macierzy rozrzedzonej, gdyż najczęściej związki pomiędzy niewiadomymi w równaniu nie dotyczą wszystkich, tylko wybranej grupy.

Przykład: Macierz z kolekcji Boeinga

Spójrzmy na macierz sztywności dla modelu silnika lotniczego, wygenerowaną swego czasu w zakładach Boeinga i pochodzącą z dyskretyzacji pewnego równania różniczkowego cząstkowego. Pochodzi z kolekcji Tima Davisa. Jest to mała macierz, wymiaru 8032 (w kolekcji spotkasz równania z milionem i więcej niewiadomych).

Struktura niezerowych elementów macierzy bcsstk38.

Jej współczynnik wypełnienia (to znaczy, stosunek liczby niezerowych do wszystkich elementów macierzy) wynosi jedynie 0.006, a więc macierz jest bardzo rozrzedzona: w każdym wierszu są średnio tylko 44 niezerowe elementy.


Reprezentacja macierzy rzadkich

Zacznijmy od sposobu przechowywania macierzy rozrzedzonych. Naturalnie, nie ma sensu przechowywać wszystkich zerowych jej elementów: wystarczy 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 wektor, nie będziemy mnożyć przez zera!).

Format współrzędnych

Do zapamiętania macierzy A wymiaru N×M o NNZ niezerowych elementów wykorzystujemy trzy wektory: I, J --- oba typu int --- oraz V, typu double, wszystkie o długości NNZ, przy czym

AI[k],J[k]=V[k],k=0,,NNZ1
Format AIJ

W tym formacie wprowadzane są macierze rzadkie do Octave'a i MATLABa:

A = sparse(I,J,V,N,N);

Jednak wewnętrznie przechowywane są w innym formacie, o którym poniżej.

Przykład

Pokażemy jak w Octave wprowadzić macierz rozrzedzoną.

octave:10> I = [1, 1, 1, 2, 3, 1, 4]; octave:11> J = [1, 3, 2, 2, 3, 4, 4]; octave:12> V = [1, 2, 3, 4, 5, 6, 7]; octave:13> N = 4; octave:14> A = sparse(I,J,V,N,N) A = Compressed Column Sparse (rows = 4, cols = 4, nnz = 7) (1, 1) -> 1 (1, 2) -> 3 (2, 2) -> 4 (1, 3) -> 2 (3, 3) -> 5 (1, 4) -> 6 (4, 4) -> 7 octave:15> spy(A);

Strukturę jej niezerowych elementów ukaże nam polecenie spy(A). Tak właśnie zostały wygenerowane obrazki macierzy w niniejszym wykładzie.


Format spakowanych kolumn (wierszy)

Format współrzędnych nie narzucał żadnego uporządkowania elementów macierzy --- 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, dobrze 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 przechowywana w postaci trzech wektorów: AV jest wektorem typu double o długości NNZ, zawierającym kolejne niezerowe elementy macierzy A wpisywane kolumnami, AI jest wektorem typu int o długości NNZ, zawierającym numery wierszy macierzy A odpowiadających elementom z AV. Natomiast zamiast tablicy J, jak to było w formacie współrzędnych, mamy krótszy wektor typu int, AP, o długości M, zawierający na j-tym miejscu indeks pozycji w AV, od którego rozpoczynają się w AV elementy j-tej kolumny macierzy A.

Format CSC

Mamy więc zależność, przy założeniu, że AP[0]=0,

AAI[AP[j]+k],j+1=AV[AP[j]+k],j=0,,M1,k=0,,AP[j+1]1

Taki (z drobnymi modyfikacjami) format macierzy wykorzystują np. pakiety ARPACK i UMFPACK (a także, wewnętrznie, Octave i MATLAB).

Format diagonalny

Znacznie mniej uniwersalny niż poprzednie i dlatego rzadziej spotykany. Kolejne diagonale macierzy przechowujemy w kolejnych wierszach macierzy P×N, gdzie P jest liczbą niezerowych diagonal w ARN×N.

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

Uwagi praktyczne

Mnożenie macierzy w formacie AIJ przez wektor jest kilka razy wolniejsze w porównaniu do macierzy w formacie CSC/CSR (z tych dwóch oczywiście (dlaczego?) CSR jest najszybszy). Co więcej, okazuje się, że w typowych implementacjach, mnożenie macierzy rozrzedzonej (reprezentowanej np. w formacie CSC) przez wektor jest mało efektywne, mniej więcej na poziomie dziesięciu procent możliwości obliczeniowych procesora.

Jeśli już poważnie myśleć o przyspieszeniu mnożenia macierzy przez wektor (np. gdy chcemy stosować iteracyjną metodę rozwiązywania układu równań z tą macierzą), warto rozważyć inne formaty --- łączące w sobie podział macierzy na bloki (tak, jak w algorytmach BLAS) i przechowywanie (w zasadzie) tylko niezerowych elementów macierzy.

Macierze specjalne

Zajmiemy się teraz zadaniem rozwiązywania układu równań liniowych

Ax=b,

ale w sytuacji, gdy macierz A jest rozrzedzona i dużego wymiaru. Dokonamy przeglądu kilku rodzajów algorytmów mających na celu wykorzystanie rozrzedzenia macierzy dla obniżenia kosztu wyznaczenia rozwiązania układu.

Należy pamiętać, że z reguły najlepsze wyniki uzyskuje się, gdy konkretny algorytm dobierze się do konkretnej macierzy. W zastosowaniach pojawiają się m.in. macierze rzadkie o bardzo szczególnej strukturze, dla nich warto stosować wyspecjalizowane algorytmy.

Macierze taśmowe

Macierz A taka, że dla |ij|d, aij=0, nazywamy macierzą taśmową z rozstępem d, o szerokości pasma 2d+1.

Łatwo sprawdzić, że algorytm eliminacji Gaussa (bez wyboru elementu głównego) 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 oszacowanie rozstępu macierzy rozkładu LU jest równe max{2d,N} --- tak więc, dla niezbyt dużych d wciąż wynikowa macierz jest taśmowa.

W szczególności, gdy macierz jest taśmowa z pasmem o rozstępie d i jednocześnie diagonalnie dominująca, wtedy rozkład LU takiej macierzy da się wykonać w miejscu kosztem O(dN), czyli liniowym.

W LAPACKu zaimplementowano szybki solver równań z macierzami taśmowymi, DGBSV, ale wymagający specjalnego sposobu przechowywania macierzy, wykorzystującego format diagonalny.

Macierze trójdiagonalne

Szczególnym przypadkiem macierzy taśmowych są macierze trójdiagonalne, tzn. taśmowe o rozstępie d=1:

A=(a1c1b2a2c2b3a3cN1bNaN)

Zadanie rozwiązywania równań z taką macierzą

Ax=e

jest bardzo często spotykane, więc warto przytoczyć algorytm w całej okazałości --- popularnie zwany algorytmem przeganiania (niektóre źródła nazywają go algorytmem Thomasa).

Zacznijmy jednak od stwierdzenia, kiedy macierz trójdiagonalna nie wymaga wyboru elementu głównego:

Stwierdzenie

Jeśli macierz A ma słabo dominującą diagonalę, tzn.

|ai||bi|+|ci|,1iN,

(b1=0=cN) i przynajmniej dla jednego indeksu "i" mamy powyżej ostrą nierówność ">", to algorytm przeganiania jest wykonalny bez przestawień wierszy. Ponadto wymaga on 7N operacji arytmetycznych, a więc jest prawie optymalny.

Algorytm Metoda przeganiania


<math>d_1</math> = <math>a_1</math>; 
<math>f_1</math> = <math>e_1</math>;
for (i = 2; i <= N; i++)
{
	<math>l</math> = <math>b_i</math>/<math>a_{i-1}</math>;
	<math>d_i</math> = <math>a_i</math> - <math>l</math> * <math>c_{i-1}</math>;
	<math>f_i</math> = <math>e_i</math> - <math>l</math> * <math>f_{i-1}</math>;
} 
<math>x_1</math> = <math>f_N</math>;
for (i = N-1; i >= 1; i--)
	<math>x_i</math> = <math>f_i</math> - <math>c_i</math> * <math>x_{i+1}</math>;

Metody bezpośrednie

Przykład: Strzałka Wilkinsona

Rozważmy układ równań z macierzą diagonalnie dominującą postaci

A=(**********)

gdzie * oznacza jakiś niezerowy element. Łatwo sprawdzić, że chociaż wyjściowa macierz jest rozrzedzona, to zastosowanie do niej eliminacji Gaussa powoduje, że w wyniku dostajemy gęste czynniki rozkładu.

Tymczasem wystarczy odwrócić kolejność równań i numerację niewiadomych (co dla macierzy jest równoznaczne z odwróceniem porządku wierszy i kolumn, korzystając z pewnej (jakiej?) macierzy permutacji P):

A~=PAPT=(**********)

Wtedy okazuje się, że rozkład naszej macierzy nie powoduje już wypełnienia czynników rozkładu!

Właśnie na tym polega główny problem w rozwiązywaniu układów z macierzami rzadkimi metodami bezpośrednimi: jak maksymalnie wykorzystać rozrzedzenie macierzy tak, by czynniki rozkładu były możliwie mało wypełnione. Albowiem wiedząc to będziemy mogli ograniczyć się jedynie do fizycznego wyznaczenia wartości niezerowych elementów macierzy rozkładu. Ponadto wymagania pamięciowe algorytmu nie będą istotnie wykraczać ponad ilość pamięci potrzebnej na przechowanie danych (tzn. macierzy).

W ogólnym przypadku rozwiązanie takiego zadania jest trudne i większość algorytmów opiera się na pewnych heurystykach, które jednak w praktyce warto wspomóc wcześniejszą analizą konkretnego układu równań jaki mamy rozwiązać. Najczęściej dąży się do takiego przenumerowania równań i niewiadomych, by w efekcie z góry przewidzieć, 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 BLAS Level 3, a więc permutacje wierszy i kolumn macierzy muszą to także brać pod uwagę.

Stosuje się kilka strategii wyznaczania korzystnych permutacji (reorderingu), z których warto wymienić

  • przybliżone algorytmy minimalnego stopnia (approximate minimum degree, AMD), np. AMD;
  • techniki podziału grafów na (prawie) rozłączne składowe nested dissection, ND, np. METIS.

Używają ich biblioteki takie jak UMFPACK, czy HSL.

W Octave mamy do dyspozycji także kilka procedur generujących takie permutacje, w tym: colamd (AMD dla macierzy niesymetrycznych) oraz symamd (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, podczas gdy standardowe podejścia nie dadzą efektu.

Przykład

Najprostszy sposób na wykorzystanie metody bezpośredniego rozwiązywania układu z macierzą rzadką to zastosowanie znanego nam operatora do macierzy typu rozrzedzonego:

A = sparse(...);
x = A \ b;

Octave domyślnie przyłoży do A reordering colamd i następnie skorzysta z biblioteki UMFPACK by rozwiązać taki układ. Dodatkowo, badane jest wcześniej, czy macierz nie jest przypadkiem taśmowa o wąskiej wstędze: jeśli jest, to korzysta się z LAPACKa.

(Jak widzisz, operator rozwiązywania układu równań jest mocno przeciążony: działa i na macierzach kwadratowych, i na prostokątnych, i na rozrzedzonych --- na każdym rodzaju w inny sposób. Nie dziwi Cię chyba, dlaczego wygodnie było napisać Octave w C++?)

Przykład: Wypełnienie pewnej macierzy w zależności od użytego reorderingu

Rozważmy ponownie macierz pochodzącą z kolekcji Tima Davisa. Jest to macierz symetryczna, dodatnio określona, wymiaru 8032, o 355460 elementach niezerowych i, w konsekwencji, o wypełnieniu około 0.6 procent.

Struktura niezerowych elementów macierzy.

Zobaczymy, jak w zależności od użytego algorytmu permutacji kolumn i wierszy poradzi sobie algorytm rozkładu Cholesky'ego.

Czynnik rozkładu Cholesky'ego L wykonanego standardowym algorytmem. Czas rozkładu: 0.892013
Czynnik rozkładu Cholesky'ego L z reorderingiem COLAMD. Czas rozkładu: 0.813038
Czynnik rozkładu Cholesky'ego L z reorderingiem SYMAMD. Czas rozkładu: 0.487683s. Prawie dwa razy szybciej niż bez reorderingu, chociaż i tak wskutek wzrostu wypełnienia macierzy w dolnym trójkącie mamy aż 2 procent niezerowych elementów.
Czynnik rozkładu Cholesky'ego L z odwrotnym reorderingiem Cuthill-McKee. Czas rozkładu: 0.845928
Czynnik rozkładu Cholesky'ego L z jeszcze innym (bardzo tanim, ale jak widać czasem zupełnie złym) reorderingiem. Czas rozkładu: 5.947936

Na zakończenie popatrzmy, jak ważne jest spostrzeżenie symetrii macierzy:

Rozkład LU, czynnik L (bez reorderingu). Czas rozkładu LU: 1.696018s. Nieakceptowalny, podobnie jak drastyczne wypełnienie macierzy.
Rozkład LU, czynnik U (bez reorderingu)

Jak widać, w naszym przypadku standardowe algorytmy (COLAMD i SYMAMD) poradziły sobie całkiem nieźle, chociaż wypełnienie i tak znacząco wzrosło. Zapewne, dla tej macierzy, która jest specyficznego typu --- pochodzi z dyskretyzacji równania różniczkowego --- algorytm ND mógłby tu jeszcze lepiej zadziałać.

Stacjonarne metody iteracyjne

Gdy macierz jest rozrzedzona, mnożenie takiej macierzy przez wektor jest bardzo tanie (koszt jest proporcjonalny do liczby niezerowych elementów macierzy). Dlatego, jeśli możemy zadowolić się rozwiązaniem przybliżonym układu, a w zamian osiągnąć je tanim kosztem, warto rozważyć metody iteracyjne.

Najprostsze metody iteracyjne (najprostsze w analizie i implementacji, ale --- jak można się domyślić --- w praktyce najmniej efektywne) polegają na rozkładzie macierzy na część "łatwo odwracalną", M, i "resztę", Z. Dokładniej, jeśli M jest nieosobliwa, to równanie Ax=b można zapisać jako zadanie punktu stałego

Mx=Nx+b,

gdzie Z=MA. Inaczej:

x=M1(Zx+b),

i zastosować doń metodę iteracji prostej Banacha:

xk+1=M1(Zxk+b)

Takie metody nazywamy stacjonarnymi metodami iteracyjnymi. Aby przeanalizować zbieżność takiej metody, warto rozpatrzyć przypadek ogólniejszy

xk=Bxk1+c,

dla pewnej macierzy B oraz wektora c. (Dla stacjonarnej metody iteracyjnej, B=M1Z oraz c=M1b.)

W tym przypadku

xkx*=Bk(x0x*),

a stąd i z nierówności BkBk, mamy

xkx*Bkx0x*.

Warunkiem dostatecznym zbieżności iteracji prostych jest więc B<1. Okazuje się, że warunkiem koniecznym i dostatecznym zbieżności tej iteracji dla dowolnego wektora startowego x0 jest

ρ=max{|λ|:λ jest wartością własną B}<1

Tak więc, metody oparte na iteracji prostej będą zbieżne liniowo z ilorazem ρ.

Zaletą stacjonarnych metod iteracyjnych jest również ich prostota, przez co są one łatwe do zaprogramowania, co łatwo zobaczyć na przykładach metod: Jacobiego i Gaussa--Seidela, które teraz omówimy.

Metoda Jacobiego

Biorąc M=diag(A), gdzie diag(A) jest macierzą diagonalną składającą się z wyrazów stojących na głównej przekątnej macierzy A, układ Ax=b jest równoważny układowi

Mx=Zx+b,

a stąd (o ile na przekątnej macierzy A nie mamy zera) otrzymujemy metodę iteracyjną

xk=Bxk1+c,

gdzie B=M1Z i c=M1b, zwaną metodą Jacobiego.

Rozpisując ją po współrzędnych dostajemy (numer iteracji wyjątkowo zaznaczamy w postaci górnego indeksu) układ rozszczepionych równań:

xi(k)=1aii(bijiaijxj(k1)),

co znaczy dokładnie tyle, że w i-tym równaniu wyjściowego układu przyjmujemy za współrzędne x wartości z poprzedniej iteracji i na tej podstawie wyznaczamy wartość xi.

Twierdzenie O zbieżności metody Jacobiego

W metodzie Jacobiego warunek dostateczny zbieżności, B<1, jest spełniony np. wtedy, gdy macierz A ma dominującą przekątną, tzn. gdy

|ai,i|>ji|ai,j|,i=1,,N

Dowód

Rzeczywiście, ponieważ wyraz (i,j) macierzy M1Z wynosi 0 dla i=j oraz ai,j/ai,i dla ij, a więc

Parser nie mógł rozpoznać (nieznana funkcja „\lefteqn”): {\displaystyle \begin{align} \lefteqn{\|M^{-1}Z\|_\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, \end{align}}

przy czym ostatnia nierówność wynika z warunku diagonalnej dominacji.

Przykład: Macierz laplasjanu

Macierz N×N, zwana macierzą jednowymiarowego laplasjanu

L=(2112112)

pojawia się w bardzo wielu zastosowaniach, także jako podzadanie w algorytmach numerycznych. Ta macierz jest macierzą taśmową, symetryczną i dodatnio określoną, więc układ równań z tą macierzą można bez trudu rozwiązać metodami bezpośrednimi, kosztem O(N). Stosując do niej metodę Jacobiego mamy M=2I oraz Z=LM. Obliczając normę macierzy iteracji Jacobiego dostajemy ||M1Z||=1, co nie rozstrzyga jeszcze o jej zbieżności lub niezbieżności.

Potrzebna będzie bardziej subtelna analiza, Okazuje się, że są znane wzory na wartości własne macierzy L:

λj=4sin2(jπ2(N+1)),

dla 1jN.

W konsekwencji, wartościami własnymi M1Z=12Z=12LI są liczby μi=12λi1. Ponieważ 0<μi<1, znaczy to, że metoda Jacobiego jest zbieżna dla macierzy L.

Z drugiej strony, nie dajmy się zwieść optymizmowi matematyka ("nie martw się, jest zbieżny..."): nietrudno sprawdzić, że ||Z||2=1O(N2)<1, co oznacza, że metoda Jacobiego --- choć zbieżna --- dla dużych N staje się zbieżna tak wolno, że w praktyce bezużyteczna!

Metoda Gaussa-Seidela

Heurystyka tej metody opiera się na zmodyfikowaniu metody Jacobiego tak, by w każdym momencie iteracji korzystać z najbardziej "aktualnych" współrzędnych przybliżenia rozwiązania x.

Rzeczywiście, przecież rozwiązując równanie metody Jacobiego:

xi(k)=1aii(bijiaijxj(k1)),

nietrudno zauważyć, że w części sumy moglibyśmy odwoływać się do "dokładniejszych" wartości xj(k): dla j<i, tzn.

xi(k)=1aii(bij<iaijxj(k)j>iaijxj(k1))

W języku rozkładu macierzy A=MZ i iteracji xk+1=M1(Zxk+b) mamy więc M=tril(A) (dolny trójkąt macierzy A).

Twierdzenie O zbieżności metody Gaussa-Seidela

Jeśli macierz A jest diagonalnie dominująca, to metoda Gaussa--Seidela jest zbieżna dla dowolnego wektora startowego x0.

Metoda Gaussa--Seidela jest w wielu przypadkach rzeczywiście szybciej zbieżna od metody Jacobiego, np. tak jest w przypadku macierzy jednowymiarowego Laplasjanu. Wciąż jednak, dodajmy, dla zadań bardzo źle uwarunkowanych jej zbieżność jest zbyt wolna by ją stosować jako samodzielną metodę.

Uwaga

Obie metody, Jacobiego i (zwłaszcza) Gaussa--Seidela stosuje się także czasem w prostych algorytmach rozwiązywania układów równań nieliniowych: ich zaletą jest to, że głównym składnikiem iteracji jest rozwiązywanie skalarnego równania nieliniowego na każdym kroku metody.

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 x*, przez złożoność metody będziemy rozumieli koszt kombinatoryczny obliczenia xk z zadaną dokładnością ϵ>0. Dla uproszczenia założymy, że medoda jest zbieżna liniowo z ilorazem ρ. Zauważmy, że aby zredukować błąd początkowy do ϵ>0, wystarczy wykonać k=k(ϵ) iteracji, gdzie k spełnia

ρkx0x*ϵ,

czyli

klog(1/ϵ)log(1/x0x*)log(1/ρ)

Liczba ta zależy więc w istotny sposób od błędu początkowego i (przede wszystkim) od współczynnika redukcji błędu ρ, natomiast zależność od dokładności ϵ i wymiaru N układu jest dużo mniej istotna (w zadaniach praktycznych -- takich jak jednowymiarowy laplasjan --- jednak często okazuje się, że... ρ zależy od N!).

Zakładając, że koszt jednej iteracji wynosi c=c(N) (c(N) jest tym mniejszy, im mniejsza jest liczba niezerowych elementów macierzy A), złożoność metody jest proporcjonalna do

c(N)log(1/ϵ)log(1/ρ).

Stąd oczywisty wniosek (prawdziwy nie tylko dla metod stacjonarnych), że metody iteracyjne warto stosować zamiast metod bezpośrednich w przypadku, gdy

  • wymiar N układu Ax=b jest "duży", oraz
  • macierz A układu jest "rozrzedzona", tzn. ma stosunkowo niewielką liczbę elementów niezerowych, np. proporcjonalną do N.

Układy o tych własnościach powstają często przy numerycznym rozwiązywaniu równań różniczkowych cząstkowych.

Metody przestrzeni Kryłowa

Zupełnie inny pomysł na realizację metody iteracyjnej przedstawiają metody przestrzeni Kryłowa, gdzie kolejne przybliżenie xk dobiera się w taki sposób, by minimalizowało pewną miarę błędu na podprzestrzeni Kryłowa

Kk=span{r,Ar,,Ak1r},

gdzie r=bAx0 jest residuum na początku iteracji. (Zwróć uwagę, że przestrzeń Kryłowa jest rozpięta przez kolejne wektory metody potęgowej] --- to nie przypadek!). W zależności od wyboru sposobu miary błędu, dostajemy inną metodę iteracyjną, takie jak CG, GMRES, PCR, BiCG, i inne. Tutaj omówimy pokrótce tylko najpopularniejszą: CG.

CG

Metoda gradientów sprzężonych, w skrócie CG (conjugate gradients), działa przy założeniu, że A jest symetryczna i dodatnio określona.

Kolejne przybliżenie xk ma minimalizować błąd w normie energetycznej indukowanej przez A,

||xkx||A=(xkx)TA(xkx)

na przestrzeni afinicznej x0+Kk. Okazuje się (co nie jest oczywiste --- trzeba skorzystać z rozmaitych własności ortogonalności generowanych wektorów), że takie zadanie minimalizacji daje się bardzo efektywnie rozwiązać, skąd dostajemy bardzo zwarty algorytm:

Algorytm Metoda CG


r = b-A*x;
<math>\rho_0</math> = <math>||r||_2^2</math>; <math>\beta</math> = 0; k = 1;
while (!stop)
{
	p = r + <math>\beta</math>*p;
	w = A*p;
	<math>\alpha</math> = <math>\rho_{k-1}</math>/<math>p^Tw</math>;
	x = x + <math>\alpha</math>*p;
	r = r - <math>\alpha</math>*w;
	<math>\rho_k</math> = <math>||r||_2^2</math>;
	<math>\beta</math> = <math>\frac{\rho_{k}}{\rho_{k-1}}</math>;
	k++;	
}

Jak widać, całą iterację da się wykonać przechowując w pamięci tylko kilka wektorów (a nie, jak możnaby się obawiać, całą przestrzeń Kk), a najdroższym jej elementem jest mnożenie macierzy przez wektor.

Twierdzenie O zbieżności CG jako metody bezpośredniej

Niech ARN×N będzie symetryczna i dodatnio określona. Algorytm CG znajdzie dokładne rozwiązanie po co najwyżej N iteracjach.

Powyższe twierdzenie, choć teoretycznie interesujące, ma małą wartość praktyczną z dwóch powodów:

  • dla bardzo dużych N, wykonanie N iteracji może być wciąż zbyt kosztownym zadaniem;
  • ponieważ w arytmetyce skończonej precyzji ortogonalność --- z której w bardzo istotny sposób korzysta się przy wyprowadzeniu algorytmu --- pogarsza się z iteracji na iterację i w konsekwencji, po wielu iteracjach, jakość xk przestaje się poprawiać.

Dlatego wygodniej potraktować CG jako metodę iteracyjną. Zachodzi bowiem

Twierdzenie Zbieżność CG jako metody iteracyjnej

Po k iteracjach metody CG,

||xkx||A2(cond(A)1cond(A)+1)k||x0x||A,

gdzie cond(A)=||A||2||A1||2.

GMRES

Metoda GMRES (Generalized Minimum RESidual) nie wymaga ani symetrii, ani dodatniej określoności macierzy, jest więc bardziej uniwersalna, choć też bardziej kosztowna od CG. Jej szczegółowe omówienie, w tym --- oszacowania szybkości zbieżności --- wykracza niestety poza ramy niniejszego wykładu.

Ściskanie macierzy

Zbieżność wszystkich poznanych metod iteracyjnych zależy od własności spektralnych macierzy układu. Pojawiające się w zastosowaniach macierze często 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

Ax=b

z macierzą o niekorzystnych własnościach, do układu

MAx=Mb,

gdzie macierz MA ma znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej. W przypadku macierzy symetrycznych widzieliśmy, że kluczowe znaczenie dla zbieżności metody miał rozkład wartości własnych: jeśli wartości własne były bardzo rozrzucone po prostej, to uwarunkowanie było bardzo duże i w konsekwencji zbieżność powolna. Aby zbieżność była szybsza, kluczowe jest, by:

  • wartości własne MA były upakowane w klastrach
  • najlepiej wszystkie w (małym) otoczeniu wartości 1

Jeśli więc chcielibyśmy przekształcić macierz tak, by metoda iteracyjna dla MA zbiegała szybko, musimy w jakiś sposób "ścisnąć" spektrum macierzy A w okolice jedności. Taką operację nazywamy ściskaniem (preconditioning), a macierz M --- imadłem.

Aby całość miała sens, macierz ściskająca M 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: M(Ax)),
  • macierz MA powinna mieć znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej.

Kilka ekstremalnych propozycji na macierz imadła to M=I (łatwa w konstrukcji i tania w mnożeniu, ale niestety nic nie polepsza...) oraz M=A1 (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 (aczkolwiek wciąż nie najbardziej skutecznych) sposobów ściskania są te oparte na zastosowaniu jednego kroku klasycznej metody iteracyjnej.

Zbieżność metody CG bez żadnego ściskania oraz ściśniętej imadłem opartym na jednej iteracji (blokowej) metody Jacobiego.

Inne sposoby ściśnięcia macierzy wykorzystują 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 imadła.

Literatura

W celu dogłębnego zapoznania się z omawianym na wykładzie materiałem, przeczytaj rozdział 4.6 i 4.7 w

  • D. Kincaid, W. Cheney Analiza numeryczna, Wydawnictwa Naukowo-Techniczne, Warszawa 2006, ISBN 83-204-3078-X.

Tym razem wymienione rozdziały nie wystarczą do dogłębnego zrozumienia omawianych treści. Warto więc sięgnąć po jeden z podręczników