Przykre testy: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przykry (dyskusja | edycje)
Przykry (dyskusja | edycje)
Linia 6: Linia 6:
-->
-->
   
   
=Pamięć hierarchiczna komputerów a algorytmy numeryczne=


W poprzednim wykładzie sprawdziliśmy, jaki jest koszt obliczeniowy algorytmu
<!--
eliminacji Gaussa. Jednak w obecnym świecie istotna jest nie tylko liczba
Konwertowane  z pliku LaTeX przez latex2mediawiki, zob. http://www.ii.uj.edu.pl/&nbsp;pawlik1/latex2mediawiki.php.
operacji arytmetycznych, ale także koszt pobrania danych z pamięci. Za chwilę
Niezb�dne rozszerzenia i modyfikacje oryginalnego latex2mediawiki
zobaczymy, że poprzez <strong>reorganizację kolejności obliczeń</strong> w algorytmie
wprowadzi� przykry@mimuw.edu.pl
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>
\fbox{\beginminipage {0.8\columnwidth}Do każdego wykładu podać na początku zestaw rozdziałów z podręcznika, które należy opanować, a na końcu --- kilka pozycji literatury uzupełniającej. W rozwiązaniach ćwiczeń też odsyłać do podręczników.\endminipage }
 
=Wprowadzenie do metod numerycznych=
 
<strong>Metody numeryczne</strong> to dziedzina wiedzy zajmująca się
problemami obliczeniowymi i konstrukcją algorytmów rozwiązywania
zadań matematycznych. Najczęściej, zadania obliczeniowe postawione są w
dziedzinie rzeczywistej (lub zespolonej) i dlatego mówimy o zadaniach
obliczeniowych <strong>matematyki ciągłej</strong> (w odróżnieniu od
[[Matematyka_dyskretna|matematyki dyskretnej]]).


Bez dostatecznie szybkiej pamięci procesor -- zamiast liczyć -- będzie
==Zadania metod numerycznych==
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,
Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw
obecnie cała walka idzie o to, by procesor przez cały czas <strong>miał co liczyć</strong>.
* określić <strong>dane problemu</strong> i <strong>cel obliczeń</strong>, czyli dokładnie
</blockquote>  
sformułować zadanie w języku matematyki,  
* określić <strong>środki obliczeniowe</strong> dzięki którym chcemy  osiągnąć cel,
* dla analizy zadania i sposobów jego rozwiązania wygodnie jest zdefiniować
<strong>klasę rozpatrywanych danych</strong> oraz <strong>model obliczeniowy</strong> w obrębie
którego będą działać nasze algorytmy.
 
Wbrew dość powszechnej opinii <strong>nie jest prawdą</strong>, że głównym przedmiotem metod
numerycznych jest badanie wpływu błędów zaokrągleń na wynik. Raczej, głównym
celem metod numerycznych jest konstrukcja optymalnych (w jasno określonym
sensie, np. pod względem wymaganej liczby operacji, lub pod względem ilości
niezbędnej informacji, czy też pod względem dokładności uzyskiwanego wyniku)
algorytmów rozwiązywania konkretnych zadań matematycznych.


Szczególnie jest to widoczne w algorytmach, które wykonują bardzo dużo operacji
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
na dużej liczbie
<span  style="font-variant:small-caps;">Uwaga</span>  
danych --- a tak jest m.in. w algorytmach algebry liniowej takich, jak mnożenie
<div class="solution" style="margin-left,margin-right:3em;">
dwóch macierzy, czy rozwiązywanie układów równań liniowych: te algorytmy
najczęściej operują na <math>\displaystyle O(N^2)</math> danych i wykonują aż <math>\displaystyle O(N^3)</math> działań.


Ponieważ szybka pamięć komputerowa jest jednocześnie bardzo droga,
Nasz przedmiot ma różne wcielenia i z tego powodu czasem nosi inne nazwy, w
konstruktorzy komputerów osobistych stoją przed wykluczającymi się celami: z
zależności od tego, na jaki aspekt metod obliczeniowych jest położony największy
jednej strony, powinni zapewnić użytkownikowi jak najwięcej pamięci, z drugiej
nacisk.
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ęć
; metody numeryczne
hierarchiczna. Koncept polega na tym, że część pamięci, która najczęściej komunikuje się z  procesorem,
: --- główny nacisk idzie na aspekty algorytmiczne;
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
; analiza numeryczna
duża-wolna), w komputerze występuje wiele poziomów:
--- przede wszystkim badanie właściwości algorytmów, ich optymalności oraz wpływu arytmetyki zmiennopozycyjnej na jakość uzyskanych wyników;
* rejestry procesora
* ''cache''(pamięć podręczna) procesora
* ''cache''drugiego poziomu (ostatnio także wbudowywana do procesora)
* pamięć operacyjna (RAM): główna pamięć komputera
* pamięć zewnętrzna (np. twardy dysk)
* pamięć masowa (CD-ROM, taśma streamera, itp.)
Efektywność działania komputera, zwłaszcza w wielkoskalowych obliczeniach
numerycznych, bardzo istotnie zależy od skutecznego wykorzystania hierarchii
pamięci tak, by jak największa część operacji była wykonywana na zmiennych
znajdujących się w danej chwili w jak najszybszej pamięci procesora.


Kluczem do skutecznego wykorzystania hierarchii pamięci jest zasada
; matematyka obliczeniowa
lokalności w czasie i w przestrzeni:
:  --- głównie teoretyczna analiza możliwości taniej i dokładnej aproksymacji rozwiązań zadań matematycznych;


<blockquote  style="background-color: #fefeee; padding:1em; margin-left,margin-right:2em;  margin-top,margin-bottom: 1em;"> 
; obliczenia naukowe
* <strong>Lokalność w czasie:</strong> Używać danego fragmentu pamięci intensywnie, ale rzadko.
:  --- nacisk na praktyczne zastosowania metod numerycznych, symulacje, realizacje na komputerach o dużej mocy obliczeniowej.
* <strong>Lokalność w przestrzeni (adresowej):</strong> W danej chwili, odnosić się do adresów pamięci leżących blisko siebie.
   
   
</blockquote>  
Oczywiście, granice podziału nie są ostre i najczęściej typowy wykład z tego
przedmiotu stara się pokazać pełne spektrum zagadnień z nim związanych. Tak
będzie również i w naszym przypadku.
 
</div></div>


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


==Jak napisać kod źle wykorzystujący pamięć podręczną?==
Tworząc i analizując algorytmy, jakie będą pojawiać w naszym wykładzie, będziemy
posługiwać się pewnym uproszczonym modelem obliczeń, dzięki czemu będziemy mogli
skoncentrować się na <strong>esencji</strong> algorytmu, bez detali implementacyjnych ---
zostawiając je na inną okazję (dobra implementacja konkretnego algorytmu może być
sama w sobie interesującym wyzwaniem programistycznym; często bywa, że dobre
implementacje, nawet prostych algorytmów numerycznych, są mało czytelne).


Choć programista nie ma bezpośredniego wpływu na opisane powyżej działania
Aby zdefiniować nasz model obliczeniowy, posłużymy się
systemu operacyjnego i ''hardware'''u (zarządzających, odpowiednio, pamięcią
pojęciem <strong>programu</strong>. Zastosujemy przy tym notację
wirtualną i ''cache''), to przez właściwe projektowanie algorytmów -- a
podobną do tej z języka programowania <strong>C</strong>.
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
Program składa się z <strong>deklaracji</strong>, czyli opisu obiektów,
użyciu kilku <strong>matematycznie równoważnych</strong> algorytmów (nazwaliśmy je umownie
których będziemy używać, oraz z <strong>poleceń</strong> (<strong>instrukcji</strong>),  
ijk, ikj, bikj(<math>\displaystyle \cdot</math>) --- nazwy pochodzą od sposobu organizacji pętli, zob.
czyli opisu akcji, które będziemy wykonywać.  
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
Dostępnymi obiektami są <strong>stałe</strong> i <strong>zmienne</strong> typu
|+ <span style="font-variant:small-caps"> </span>
całkowitego (<code>int</code>),
|-
rzeczywistego (<code>float</code> i <code>double</code>). Typ logiczny symulujemy tak jak w
|
C wartościami zero-jedynkowymi typu całkowitego.
Algorytm  ||  ijk  ||  ikj  ||  bikj(16)  ||  bikj(32) ||  DGEMM  ||  ATLAS DGEMM
|-
Zmienne jednego typu mogą być grupowane w wektory albo tablice.  
|
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
|-
|


|}
Widzimy więc, że podstawowe algorytmy numeryczne będą bazować na mało
skomplikowanych typach danych. Również nieskomplikowane będą instrukcje naszego
modelowego języka. Dążenie do prostoty (ale nie za wszelką cenę) jest charakterystyczne dla numeryki. Typowe jest zastępowanie skomplikowanych tablic rekordów prostszymi macierzami, eleganckich rekurencji --- zwykłymi pętlami działającymi na danych w miejscu. Jedną z myśli przewodnich numeryki jest bowiem <strong>szybkość</strong> i ta rezygnacja z barokowych konstrukcji językowych jest tu analogiczna do sposobu konstrukcji architektury procesora RISC, pod hasłem ''efekywność przez prostotę''.


Jak widać, różnice pomiędzy --- podkreślmy, matematycznie równoważnymi ---
(Na pewno zastanawiasz się teraz, jaka jest ''druga'' myśl przewodnia numeryki. To <strong>dokładność</strong>.)
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'''
===Podstawienie===


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Najprostsza z instrukcji, bez której nie można się obejść:
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
   
   
/* ijk */
  z = <math>\displaystyle {\cal W}</math>;
for (i = 0; i < N; i++)
</div>
for (j = 0; j < N; j++)
for (k = 0; k < N; k++)
C[i*N+j] += A[i*N+k]*B[k*N+j];
</pre></div>
   
   
Jest to algorytm, który zapewne większości z Czytelników pierwszy przyszedłby
Tutaj <math>\displaystyle z</math> jest zmienną, a <math>\displaystyle {\cal W}</math> jest <strong>wyrażeniem</strong>
do głowy, gdyż realizuje wprost powszechnie znaną regułę mnożenia macierzy
o wartościach tego samego typu co <math>\displaystyle z</math>. Jest to polecenie proste.
"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>,
Wyrażeniem jest pojedyncza stała lub zmienna, albo złożenie
''cache miss'' następuje praktycznie w każdym kroku. Dzieje się tak dlatego,
skończonej liczby <strong>operacji elementarnych</strong> na wyrażeniach.
że wymiary naszych macierzy są wielokrotnością 512: odwołując się
Operacje elementarne to:  
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'''
; arytmetyczno--arytmetyczne:
:  <math>\displaystyle x\mapsto -x</math>, <math>\displaystyle (x,y)\mapsto x+y</math>,
<math>\displaystyle (x,y)\mapsto x-y</math>, <math>\displaystyle (x,y)\mapsto x*y</math>,
<math>\displaystyle (x,y)\mapsto x/y, y\ne 0</math>, gdzie <math>\displaystyle x,y</math> są stałymi lub
zmiennymi liczbowymi,


Różni się on od poprzedniego jedynie
; arytmetyczno--logiczne:
kolejnością dwóch wewnętrznych pętli:
: <math>\displaystyle (x,y)\mapsto x<y</math>,
<math>\displaystyle (x,y)\mapsto x\le y</math>, <math>\displaystyle (x,y)\mapsto x=y</math>, <math>\displaystyle (x,y)\mapsto x\ne y</math>,
gdzie <math>\displaystyle x,y</math> są stałymi lub zmiennymi liczbowymi,


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
; logiczno--logiczne:
: <math>\displaystyle p\mapsto\,\sim \,p</math>,
<math>\displaystyle (p,q)\mapsto p\, \wedge \,q</math>, <math>\displaystyle (p,q)\mapsto p\, \vee \,q</math>,  
gdzie <math>\displaystyle p,q</math> są stałymi lub zmiennymi logicznymi.
   
   
/* ikj */
Dla niektórych zadań wygodnie jest (a czasem konieczne)
for (i = 0; i < N; i++)
uzupełnić ten zbiór o dodatkowe operacje, takie jak
for (k = 0; k < N; k++)
obliczanie wartości niektórych standardowych funkcji matematycznych
for (j = 0; j < N; j++)
(<math>\displaystyle \sqrt{\;}, \cos(), \sin(), \exp(), \log(),</math> itp.),
C[i*N+j] += A[i*N+k]*B[k*N+j];
czy nawet funkcji bardziej skomplikowanych. Na przykład,
</pre></div>
zastosowanie "szkolnych" wzorów na obliczanie pierwiatków
równania kwadratowego byłoby
niemożliwe, gdyby pierwiastkowanie było niemożliwe.
   
   
Okazuje się, że taka prosta zmiana dramatycznie poprawia sytuację, zmniejszając
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
liczbę ''cache misses''!
<span  style="font-variant:small-caps;">Uwaga</span>
<div class="solution" style="margin-left,margin-right:3em;">
 
Należy pamiętać, że w praktyce funkcje  standardowe (o ile są
dopuszczalne) są obliczane używając  czterech podstawowych operacji
arytmetycznych.  Dokładniej, jednostka arytmetyczna procesora potrafi wykonywać
jedynie operacje <math>\displaystyle +,\,-,\,\times,\,\div</math>, przy czym dzielenie zajmuje kilka razy
więcej czasu niż pozostałe operacje arytmetyczne. Niektóre procesory (np. Itanium 2) są w
stanie wykonywać niejako jednocześnie operację dodawania i mnożenia (tzw. FMADD,
fused multiply and add). Praktycznie wszystkie współczesne procesory mają także
wbudowany koprocesor matematyczny, realizujący asemblerowe polecenia wyznaczenia
wartości standardowych funkcji matematycznych (<math>\displaystyle \sqrt{\;}, \cos(), \sin(),
\exp(), \log(),</math> itp.), jednak wykonanie takiej instrukcji wymaga mniej więcej
kilkadziesiąt razy więcej czasu
niż wykonanie operacji dodawania.
</div></div>


'''Algorytm bikj()'''
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Uwaga</span>
<div class="solution" style="margin-left,margin-right:3em;">


Algorytm bikj(16) jest prostą wersją algorytmu blokowego operującego w sposób
Niestety, aby nasz  model obliczeniowy wiernie odpowiadał rzeczywistości, musimy
"ikj" na blokach macierzy wymiaru <math>\displaystyle 16\times 16</math>:
w nim uwzględnić fakt, że działania matematyczne (i, tym bardziej,
obliczanie wartości funkcji matematycznych) <strong>nie są</strong> wykonywane dokładnie.
Czasem
uwzględnienie tego faktu wiąże się ze znaczącym wzrostem komplikacji analizy
algorytmu i dlatego "w pierwszym przybliżeniu" często pomija się to
ograniczenie przyjmując model w którym wszystkie (lub prawie wszystkie)
działania arytmetyczne wykonują się dokładnie. Wiedza o tym, <strong>kiedy</strong> i
<strong>jak</strong> zrobić to tak, by wciąż wyciągać prawidłowe wnioski odnośnie faktycznej
realizacji algorytmów w obecności błędów zaokrągleń jest częścią sztuki i wymaga
intuicji numerycznej, popartej doświadczeniem.
</div></div>


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane.
 
===Warunkowe===
 
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
   
   
/* bikj(16) */
if(<math>\displaystyle \cal W</math>)  
for (i = 0; i < N; i+=16)
<math>\displaystyle {\cal A}_1</math>;
for (k = 0; k < N; k+=16)
else
for (j = 0; j < N; j+=16)
<math>\displaystyle {\cal A}_2</math>;
for (ii = i; ii < i+15; ii++)
</div>
for (kk = k; kk < k+15; kk++)
for (jj = j; jj < j+15; jj++)
gdzie <math>\displaystyle {\cal W}</math> jest wyrażeniem o wartościach całkowitych (0 odpowiada
C[ii*N+jj] += A[ii*N+kk]*B[kk*N+jj];
logicznemu fałszowi, inne wartości --- logicznej prawdzie), a <math>\displaystyle {\cal A}_1</math>
i <math>\displaystyle {\cal A}_2</math> są poleceniami, przy czym dopuszczamy polecenia puste.


</pre></div>
===Powtarzane===
 
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
while(<math>\displaystyle {\cal W}</math>)
<math>\displaystyle {\cal A}</math>;
</div>
   
   
(podobnie działał algorytm bikj(32), tym razem na blokach <math>\displaystyle 32\times 32</math>).
gdzie <math>\displaystyle W</math> jest wyrażeniem o wartościach logicznych, a <math>\displaystyle \cal A</math>  
jest poleceniem.  


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


'''Algorytmy DGEMM i ATLAS DGEMM'''
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
{
<math>\displaystyle {\cal A}_1;\displaystyle {\cal A}_2;\displaystyle \ldots\displaystyle {\cal A}_n;</math>
}
</div>
gdzie <math>\displaystyle {\cal A}_j</math> są poleceniami.


Algorytm DGEMM to był algorytm mnożenia macierzy z pakietu BLAS --- jest to
Na podstawie tych trzech poleceń można tworzyć inne, takie
właśnie profesjonalny algorytm blokowy, ale niezoptymalizowany na naszą
jak pętle <code>for()</code>, czy <code>switch()</code>, itd.  
architekturę. Dopiero algorytm DGEMM podrasowany w pakiecie ATLAS dał nam
sprawność wynoszącą 1.2 Gflopów na sekundę -- czyli praktycznie
maksimum (''teoretycznie'', z Durona można wycisnąć dwa flopy w cyklu
zegara, co dawałoby <math>\displaystyle r_{\max}</math> = 2.2 Gflop/s, ale w praktyce jest to mało
prawdopodobne) tego,
co da się wycisnąć z tej wersji Durona na liczbach podwójnej precyzji.


==Reprezentacja macierzy gęstych==
Mamy też dwa szczególne polecenia, które odpowiadają
za "wejście" i "wyjście".


Do implementacji algorytmów numerycznych powszechnie używa się dwóch języków:
===Wprowadzanie danych===
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
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
mamy do czynienia z macierzą prostokątną <math>\displaystyle n\times m</math> o elementach <math>\displaystyle a_{ij}</math>,
<math>\displaystyle i=1\ldots n</math>, <math>\displaystyle j=1\ldots m</math>,
  <math>\displaystyle {\cal IN}</math>(x,t);
</div>
gdzie <math>\displaystyle x</math> jest zmienną rzeczywistą, a <math>\displaystyle t</math> "adresem" pewnego
funkcjonału <math>\displaystyle L:F\toR</math> należącym to pewnego zbioru <math>\displaystyle T</math>.
W wyniku wykonania tego polecenia w zmiennej <math>\displaystyle x</math> zostaje
umieszczona wartość <math>\displaystyle L_t(f)</math>.


<center><math>\displaystyle  
Polecenie to pozwala zdobyć <strong>informację</strong> o danej <math>\displaystyle f</math>.
\begin{pmatrix}  
Jeśli <math>\displaystyle F=R^n</math> to zwykle mamy <math>\displaystyle T=\{1,2,\ldots,n\}</math> i
a_{11} & \cdots & a_{1m}\\
<math>\displaystyle L_i(f)=f_i</math>, co w praktyce odpowiada wczytaniu <math>\displaystyle i</math>-tej
\vdots &        & \vdots\\
współrzędnej wektora danych. W szczególności, ciąg poleceń
a_{n1} & \cdots & a_{nm}
<math>\displaystyle {\cal IN}(x[i],i)</math>, <math>\displaystyle i=1,2,\ldots,n</math>, pozwala uzyskać pełną
\end{pmatrix} .
informację o <math>\displaystyle f</math>. Jeśli zaś <math>\displaystyle F</math> jest pewną klasą
</math></center>
funkcji <math>\displaystyle f:[a,b]\toR</math>, to możemy mieć np. <math>\displaystyle T=[a,b]</math> i <math>\displaystyle L_t(f)=f(t)</math>.
W tym przypadku, wykonanie polecenia <math>\displaystyle {\cal IN}(x,t)</math> odpowiada
w praktyce skorzystaniu ze specjalnej procedury (albo urządzenia
zewnętrznego) obliczającej (mierzącego) wartość funkcji <math>\displaystyle f</math>
w punkcie <math>\displaystyle t</math>.
 
===Wyprowadzanie wyników===


to kolejne miejsca w przestrzeni adresowej
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
zajmują elementy
<center><math>\displaystyle a_{11}, a_{21},\ldots, a_{n1}, a_{12}, a_{22}, \ldots, a_{n2},
  <math>\displaystyle {\cal OUT}</math>(<math>\displaystyle {\cal W}</math>);
\ldots a_{nm}.
</div>
</math></center>
   
   
Dla odmiany, <strong>C przechowuje w pamięci elementy macierzy wierszami</strong>, tzn. kolejno
gdzie <math>\displaystyle {\cal W}</math> jest wyrażeniem o wartościach rzeczywistych.
<center><math>\displaystyle a_{11}, a_{12},\ldots, a_{1m}, a_{21}, a_{22}, \ldots, a_{2m}, \ldots
Polecenie to pozwala "wskazać" kolejną współrzędną wyniku.
a_{nm}.</math></center>


Co więcej, standard <strong>nie gwarantuje, że kolejne wiersze macierzy będą
Zakładamy, że na początku procesu obliczeniowego wartości
przechowywane w przylegających do siebie obszarach pamięci</strong>. Bierze się to stąd,
wszystkich zmiennych są nieokreślone, oraz że dla dowolnych
że w C macierz dwuwymiarowa jest w istocie tablicą wskaźników do kolejnych
danych wykonanie programu wymaga wykonania skończonej liczby
wierszy.  
operacji elementarnych. Wynikiem obliczeń jest skończony ciąg
liczb rzeczywistych (albo <math>\displaystyle puste</math>), którego kolejne współrzędne
pokazywane są poleceniem <math>\displaystyle {\cal OUT}</math>.  


To zaś powoduje kolejne komplikacje. Otóż w procedurach numerycznych bardzo
==Środowisko obliczeniowe==
często pragniemy  dynamicznie przydzielać pamięć na tablice, to znaczy dopiero
 
w trakcie działania procedury, gdyż dopiero w trakcie obliczeń procedura
Ze względu na swój utylitarny charakter, w metodach numerycznych niemal równie
otrzymuje np. informację o rozmiarze potrzebnej macierzy. Przykładowo, program w C,
ważna jak dobór optymalnego algorytmu jest jego efektywna <strong>implementacja</strong> na
który dynamicznie przydzielałby pamięć na dwuwymiarową tablicę,
konkretnej architekturze.
musiałby:
 
* przydzielić pamięć na tablicę wskaźników do wierszy,
W praktyce mamy dwie możliwości:
* każdemu wskaźnikowi do wiersza przydzielić pamięć na pojedynczy wiersz.
* wykorzystanie standardowych języków programowania (C, Fortran, być może ze wstawkami w asemblerze) oraz wyspecjalizowanych bibliotek
* użycie gotowego środowiska obliczeń numerycznych będącego wygodnym interfejsem do specjalizowanych bibliotek numerycznych
   
   
To jest jeden z licznych powodów, dla których, posługując się dwuwymiarowymi macierzami w C, będziemy stosowali pewien prosty ''trick''.  
Zaleta pierwszego podejścia to (zazwyczaj) szybko działający kod wynikowy, ale kosztem długotrwałego i żmudnego programowania. W drugim przypadku jest na odwrót: developerka i testowanie --- wyjątkowo ważne w przypadku programu numerycznego --- postępują bardzo szybko, ale czasem kosztem ogólnej efektywności uzyskanego produktu.


Dlatego przyjmiemy konwencję, że nie będziemy wcale korzystać z macierzy
====Języki programowania: C i Fortran====
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,
Programy numeryczne (a przynajmniej ich jądra obliczeniowe) są zazwyczaj niezbyt
wybierzemy zazwyczaj układ kolumnowy (czyli fortranowski) (pamiętajmy wszak, że
wymagające jeśli chodzi o struktury danych, co więcej, prostota struktur danych
niektóre biblioteki w
szybko rewanżuje się efektywniejszym kodem. Dlatego, trawestując Einsteina, w
C (np. [http://www.fftw.org FFTW]) wymagają jednak układu wierszowego!),
dobrym programie numerycznym należy
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
<blockquote  style="background-color: #fefeee; padding:1em; margin-left,margin-right:2em;  margin-top,margin-bottom: 1em;"
większość była i często nadal jest pisana z myślą o implementacji w Fortranie.
używać tak prostych struktur danych, jak to
Złudzenie korzystania z macierzy dwuwymiarowej da nam zaś makro, lokalizujące
możliwe (ale nie prostszych!...)
<math>\displaystyle (i,j)</math>-ty element macierzy w wektorze przechowującym jej elementy. Dodatkowo,
</blockquote>  
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>.
Językami opartymi na prostych konstrukcjach programistycznych są: Fortran i C. Dlatego właśnie są to języki
dominujące współcześnie pisane programy numeryczne. O ile w przeszłości
hegemonia Fortranu była nie do podważenia, o tyle teraz coraz więcej
oprogramowania numerycznego powstaje w C.


Poniżej pokazujemy przykładowy fragment kodu realizującego opisaną wyżej ideę.
W naszym wykładzie wybieramy C ze względu na jego uniwersalność,
doskonałą przenośność i całkiem dojrzałe kompilatory. Dodajmy,
że funkcje w C można mieszać np. z gotowymi bibliotekami napisanymi w
Fortranie. Fortran, język o bardzo długiej tradycji, wciąż żywy i  mający grono wiernych fanów, jest nadal wybierany przez numeryków na całym świecie między innymi ze względu na jego dopasowanie do zadań obliczeniowych (właśnie w tym celu powstał), a także ze względu na doskonałe kompilatory dostępne na superkomputerach, będące efektem wieloletniej ewolucji i coraz lepszego nie tylko dopasowania kompilatora do spotykanych konstrukcji językowych, ale także na odwrót --- coraz lepszego zrozumienia programistów, jak pisać programy, by wycisnąć jak najwięcej z kompilatora.


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Inne popularne języki: [[Programowanie_obiektowe|Java]], Pascal (ten język, zdaje się, jest popularny już
#define N 10
tylko w obrębie [http://www.mimuw.edu.pl  wydziału MIM UW]...), VisualBasic i inne, [http://www.cs.berkeley.edu/&nbsp;wkahan/JAVAhurt.pdf  nie są zbyt odpowiednie] dla
#define IJ(i,j,n) ((i)-1+((j)-1)*(n))
obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala:
#include <stdio.h>
<code>real</code> nie jest zgodny z powszechnym standardem [[MN03|IEEE 754]]. Jednak, ze względu na coraz większą
komplikację kodów numerycznych służących np. do prowadzenia zaawansowanych
symulacji metodą elementu skończonego, coraz więcej kodów wykorzystuje
możliwości obiektowych języków C++ i Fortran90.


int main(void)
W przykładach będziemy najczęściej odnosić się do architektury x86, tzn. 32-bitowej IA-32
{
procesorów firmy [http://developer.intel.com  Intel] i [http://developer.amd.com  AMD], najczęściej spotykanej w obecnie używanych
double *matrix, *ptr;
komputerach. Należy jednak pamiętać, że obecnie następuje przejście na
int i,j;
architekturę 64-bitową. Ze względu jednak na brak pewności co do ostatecznie
przyjętych standardów w tym obszarze, ograniczymy się do procesorów
32-bitowych. W przykładach będziemy korzystać z kompilatora [http://gcc.gnu.org  GCC], który jest omówiony w wykładzie [[Środowisko_programisty|Środowisko programisty]].


matrix = (double *)malloc(N*N*sizeof(double));
====Prosty program numeryczny w C====


ptr = matrix;
Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) <math>\displaystyle N</math>-tą sumę
częściową szeregu harmonicznego
<center><math>\displaystyle
x = \sum_{i=1}^N \frac{1}{i}.
</math></center>


/* staramy się jak naczęściej odwoływać się do elementów macierzy kolumnowo */
Przyjmijmy, że parametr <math>\displaystyle N</math> będzie miał wartość równą 2006. W pierwszym odruchu,
prawie każdy początkujący student pisze program w rodzaju:


for (j=1; j<=N; j++)
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
for (i=1; i<=N; i++)
#include <stdio.h>
{
#define N 2006
*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++)
int main(void)
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++)
float x;
fprintf(stderr,"&#37;5.2g ", matrix[IJ(i,j,N)]);
unsigned int i;
fprintf(stderr,"\n");
 
}
x = 0.0;
for(i = 1; i <= N; i++)  
x = x + 1/i;
printf("Wartość sumy x = 1 + 1/2 + ... + 1/&#37;d jest równa &#37;g\n", N, x);


free(matrix);
return(0);
return(0);
}
}
</pre></div>
</div>
   
   
<!--
Sęk w tym, że ten program <strong>nie działa!</strong> To znaczy: owszem, kompiluje się i
Zwróćmy uwagę na dwa sposoby odwoływania się do elementów macierzy. Za pierwszym
uruchamia, ale twierdzi uparcie, że nasza suma wynosi... 1.
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
Winę za to ponosi linijka
odwoływać się do kolejnych <strong>wierszy</strong> macierzy (a więc, z punktu
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
wykorzystania cache i hierarchii pamięci, fatalnie! -- na szczęście I/O jest
znacznie wolniejszy od najwolniejszej nawet pamięci RAM). Tym razem więc nie
x = x + 1/i;
unikniemy wywołania makra <code>IJ()</code> (i obliczania wyrażenia <code>i+j*N</code>) przy
</div>
każdym obrocie wewnętrznej pętli.
-->
   
   
Dodajmy, że opisane podejście
w której wykonujemy dzielenie <code>1/i</code>. Obie liczby są typu <code>int</code> i
nie jest niczym nowym w praktyce numerycznej i jest stosowane w wielu dobrych
dlatego, zgodnie z regułami C, wynik ich dzielenia także jest całkowity:
bibliotekach numerycznych.
dostajemy część całkowitą z dzielenia tych dwóch liczb, czyli, gdy tylko <math>\displaystyle i >
1</math>, po prostu zero.


==Włączenie zewnętrznej biblioteki fortranowskiej do programu==
Prawidłowy program musi więc wymusić potraktowanie choć jednej z tych liczb jako
liczby zmiennoprzecinkowej, co najprościej uzyskać albo przez


Istnieje prosty sposób wykorzystania gotowych pakietów
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
fortranowskich przez zewnętrzne programy w C: wystarczy skorzystać z genialnej
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
wektora, korzystając z funkcji <code style="color: #903">DNRM2</code> biblioteki
[http://www.netlib.org/blas  BLAS].
 
Najpierw musimy zorientować się, jak wygląda schemat wywołania takiej funkcji w
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>
   
   
      DOUBLE PRECISION FUNCTION DNRM2 ( N, X, INCX )
x = x + 1.0/i;
*    .. Scalar Arguments ..
</div>
      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> --
albo bardziej uniwersalnie, rzutując choć jedną z liczb na odpowiedni typ:
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>
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
   
   
norm( X(1:INCX:N) )
x = x + 1/((float) i);
</pre></div>
</div>
   
   
Kod obiektowy tej funkcji znajduje się już w bibliotece BLAS zawartej w pliku
Poprawny kod miałby więc postać
<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?
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
#include <stdio.h>
#define N 2006


Zacznijmy od nazwy. W przypadku kompilatora
int main(void)
<code style="color: #666">gcc</code>/<code style="color: #666">gfortran</code>, nazwą funkcji do wykorzystania w C będzie
{
<code>dnrm2_</code> (tak! małymi literami i z przyrostkiem "<code>_</code>").
float x;
unsigned int i;


Jeśli zaś chodzi o argumenty, to zapewne co do drugiego nie będziemy mieli
x = 0.0;
wątpliwości: jako wektor <code style="color: #903">X</code> przekażemy -- naturalnie -- <strong>wskaźnik</strong> do
for(i = 1; i <= N; i++)
tablicy <code>X</code> (typu <code>double</code>), czyli po prostu: jej nazwę. Co z
x = x + 1.0/i;
pozostałymi argumentami? Okazuje się, że reguła jest niezmienna: każdy argument funkcji fortranowskiej zastępujemy <strong>wskaźnikiem</strong> do odpowiedniego typu:
printf("Wartość sumy x = 1 + 1/2 + ... + 1/&#37;d jest równa %g\n", N, x);


{| border=1
return(0);
|+ <span style="font-variant:small-caps"> </span>
}
|-
</div>
|
   
Fortran 77  || C
====Typy numeryczne w 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>
|-
|


|}
W języku C mamy dostępnych sześć typów numerycznych:
* stałoprzecinkowe, dla reprezentacji liczb całkowitych
** <code>int</code> oraz <code>long int</code>.  W realizacji [http://gcc.gnu.org  GCC] na komputery klasy PC, oba typy: <code>int</code> i <code>long int</code> są identyczne (32-bitowe) i ich zakres wynosi około <math>\displaystyle -2.1\cdot 10^9\ldots +2.1\cdot 10^9</math>. Typ <code>int</code> i jemu pokrewne  odnoszą się do liczb całkowitych ze znakiem (dodatnich lub ujemnych). Ich warianty bez znaku: <code>unsigned int</code>, itp. odnoszą się do liczb bez znaku (nieujemnych), dlatego np. zakresem <code>unsigned int</code> będzie w przybliżeniu <math>\displaystyle 0\ldots +4.2\cdot 10^9</math>.
** <code>long long int</code> (64-bitowy) o zakresie w przybliżeniu <math>\displaystyle -9.2\cdot 10^{18}\ldots +9.2\cdot 10^{18}</math>.
* zmiennopprzecinkowe, dla reprezentacji liczb rzeczywistych
** <code>float</code>, pojedynczej precyzji, 32-bitowy, gwarantuje precyzję około <math>\displaystyle 10^{-7}</math> i zakres liczb reprezentowalnych w przybliżeniu <math>\displaystyle 10^{-38}\ldots 10^{38}</math>;
** <code>double</code>, podwójnej precyzji, 64-bitowy, ma precyzję na poziomie <math>\displaystyle 10^{-16}</math> przy orientacyjnym zakresie <math>\displaystyle 10^{-308}\ldots 10^{308}</math>;
** <code>long double</code>, rozszerzonej podwójnej precyzji, na pecetach 80-bitowy, ale w pamięci zajmuje on 12 bajtów) o precyzji rzędu <math>\displaystyle 10^{-20}</math> i odpowiadający standardowi double extended IEEE 754.
Powyższe typy zmiennoprzecinkowe w realizacji na PC odpowiadają
[http://www.cs.berkeley.edu/&nbsp;wkahan/ieee754status/754story.html  standardowi IEEE 754]. Standardowo, operacje arytmetyczne na obu typach <code>float</code> i <code>double</code> są tak samo pracochłonne, gdyż wszystkie obliczenia w C wykonywane są z maksymalną dostępną precyzją (czyli, na procesorach architektury IA-32 Intela i AMD: w precyzji oferowanej przez typ <code>long double</code>), a następnie dopiero wynik zapisywany do zmiennej reprezentowany jest w stosownym typie . Jednakże typ pojedynczej precyzji <code>float</code> oferuje znacznie większe możliwości optymalizacji uzyskanego kodu a ponadto, obiekty typu <code>float</code> zajmują dwukrotnie mniej miejsca w pamięci niż <code>double</code>, dając możliwość lepszego wykorzystania pamięci podręcznej (''cache'')  i przetwarzania wektorowego.


A więc pierwszym i trzecim argumentem funkcji <code>dnrm2_</code>
====Stałe matematyczne i podstawowa biblioteka matematyczna====
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>
Język C jest językiem "małym" i, jak wiadomo, nawet proste operacje
wejścia-wyjścia są w istocie nie częścią języka, ale funkcjami (makrami?)
bibliotecznymi. Z drugiej strony jednak, jak zorientowaliśmy się, nie stwarza to
programiście żadnych specjalnych niedogodności. Podobnie rzecz ma się z
funkcjami matematycznymi.  Podstawowe funkcje matematyczne (<math>\displaystyle \sin, \cos, \exp,
\ldots</math>, itp.) nie są składnikami języka C, lecz w zamian są zaimplementowane w
tzw. standardowej bibliotece matematycznej <code style="color: #666">libm.a</code>; prototypy tych
funkcji oraz definicje rozmaitych stałych matematycznych: <math>\displaystyle \pi, e, \ldots</math> itp.
znajdują się w pliku nagłówkowym <code style="color: #666">math.h</code>. Aby więc skorzystać z tych
funkcji w programie, należy 
* w nagłówku pliku, w którym korzystamy z funkcji lub stałych matematycznych, umieścić linię <code>#include <math.h></code>
* przy linkowaniu dołączyć bibliotekę matematyczną za pomocą opcji <code style="color: #666">-lm</code>
   
   
double dnrm2_(int* N, double* X, int* INCX);
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
</pre></div>
<span  style="font-variant:small-caps;">Przykład</span>  
<div class="solution" style="margin-left,margin-right:3em;">
No to wykorzystajmy naszą funkcję:
 
Oto przykładowy prosty program numeryczny w C; drukuje on tablicę wartości
sinusów losowo wybranych liczb z przedziału <math>\displaystyle [0,2\pi]</math>.


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
   
  [{Tablica losowych sinusów}]
/* Wykorzystanie funkcji DNRM2 fortranowskiej biblioteki BLAS w programie w C*/
#include <math.h>
#include <stdio.h>
#include <stdio.h>  
double dnrm2_(int*,double*,int*);
#include <stdlib.h> /* zawiera definicję funkcji rand() i stałej RAND_MAX */
#define N 15 /* ile liczb wydrukować */


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


for( i = 0; i < N; i++)
{
x = rand()/(double)RAND_MAX;
x *= 2.0*M_PI;
/* oczywiście, wystarczyłoby x =(2.0*M_PI*rand())/RAND_MAX; */
y = sin(x);
fprintf(stderr, "(&#37;3d) x = &#37;10.5e sin(x) = &#37;10.5e\n", i, x, y);
}
return(0);
return(0);
}
}
</pre></div>
</div>
   
   
Zwróćmy uwagę na sposób kompilacji tego programu:  
Zwróćmy uwagę na linię <code>x = rand()/(double)RAND_MAX;</code> Funkcja <code>rand()</code>
zwraca losową liczbę całkowitą z przedziału [0,<code>RAND_MAX</code>], więc iloraz z
predefiniowaną stałą <code>RAND_MAX</code> będzie liczbą z przedziału <math>\displaystyle [0,1]</math>. Dla
prawidłowego uzyskania losowej liczby z przedziału <math>\displaystyle [0,1]</math> kluczowe jest jednak
zrzutowanie jednej z dzielonych liczb na typ <code>double</code>! Gdyby tego nie zrobić,
uzyskalibyśmy zawsze <math>\displaystyle x=0</math> lub sporadycznie <math>\displaystyle x=1</math>, zgodnie z regułą C: "<strong>typ
wyniku jest zgodny z typem argumentów</strong>". Rzeczywiście, w naszym wypadku,
(błędna) linia <code>x = rand()/RAND_MAX;</code> zostałaby wykonana tak: ponieważ
wynikiem funkcji <code>rand()</code> jest <code>int</code> i stała <code>RAND_MAX</code> jest także typu
<code>int</code>, to wynik ma być również typu <code>int</code> -- zostanie więc wykonane dzielenie
całkowite; ponieważ mamy <code>rand() <math>\displaystyle \leq</math> RAND_MAX</code>, to wynikiem będzie
albo 0, albo 1, i taki rezultat, po zamianie na typ <code>double</code>, zostanie przypisany
zmiennej <math>\displaystyle x</math>, co oczywiście nie jest naszym zamiarem. Natomiast, gdy
przynajmniej jedna z dzielonych liczb jest typu <code>double</code>, to oczywiście wynikiem
też musi być liczba typu <code>double</code>, zostanie więc wykonane zwyczajne dzielenie
dwóch liczb zmiennoprzecinkowych (wynik <code>rand()</code> automatycznie zostanie
zrzutowany na typ <code>double</code>).


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Kompilujemy ten program, zgodnie z uwagami uczynionymi na początku, poleceniem
 
<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:#fcfcfc;">
   
   
gcc -o testblas testblas.c -lblas -lgfortran -lm
gcc -o sinusy sinusy.c -lm
</pre></div>
</div>
   
   
oprócz biblioteki BLAS, co
</div></div>
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====
====Środowisko obliczeń numerycznych: MATLAB i jego klony====


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em; color:#e0e"><div style="font-family:monospace; white-space: pre; background-color:#fff; color: red; border-style: dashed; border-width: thin; padding:1em; border-color:red">
[[Image:MNmatlab-screenshot.png|thumb|550px|center|Typowa sesja MATLABa. Zwróć uwagę na edytor kodu źródłowego na
bieżąco interpretujący go i wychwytujący potencjalne błędy]]
 
W końcu lat siedemdziesiątych ubiegłego wieku, Cleve Moler wpadł na pomysł
stworzenia prostego interfejsu do ówcześnie istniejących bibliotek numerycznych
algebry liniowej: pakietów LINPACK  i EISPACK
   
   
test funkcji
(których był współautorem). Stworzone przez niego:
jak  
język skryptów i łatwe w użyciu narzędzia manipulacji macierzami, zdobyły
to  s    aaaa 
wielką popularność w środowisku naukowym. W 1984 roku Moler skomercjalizował swe
dzieło pod nazwą [http://www.mathworks.com  MATLAB] (od:
"MATrix LABoratory").
 
MATLAB wkrótce rozrósł się potężnie, implementując (lub wykorzystując) wiele
najlepszych z istniejących algorytmów numerycznych, a także oferując bogate
możliwości wizualizacji wyników. Dzięki swemu interfejsowi, składającemu się z
prostych, niemal intuicyjnych funkcji, oraz ogromnym możliwościom
jest jednym z powszechniej używanych pakietów do prowadzenia symulacji
komputerowych w naukach przyrodniczych (i nie tylko).
 
Interpreter MATLABa jest dosyć wolny, dlatego podstawową regułą pisania
efektywnych kodów w MATLABie jest maksymalne wykorzystanie gotowych
(prekompilowanych) funkcji MATLABa oraz --- zredukowanie do minimum
obecności takich struktur programistycznych jak pętle.
 
[[Image:MNoctave-screenshot.png|thumb|550px|center|Screenshot Octave. W terminalu otwarta sesja w
ascetycznym
trybie tekstowym, grafika wyświetlana z wykorzystaniem [http://www.gnuplot.info  Gnuplota]]]
 
Kierując się podobnymi przesłankami co C.&nbsp;Moler, oraz bazując na wielkim
sukcesie MATLABa, John W.&nbsp;Eaton z Wydziału Inżynierii Chemicznej Uniwersytetu
Wisconsin w USA, zaczął w 1994 roku opracowywać darmowe (udostępniane na tzw.
licencji GPL) oprogramowanie o funkcjonalności maksymalnie
zbliżonej do MATLABa: [http://www.octave.org  Octave]. Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest
intensywnie rozwijana do dziś.
 
[[Image:MNscilab-screenshot.png|thumb|550px|center|Screenshot Scilaba.]]
 
Drugim udanym klonem MATLABa jest francuski [http://www.scilab.org  Scilab],
opracowany w laboratoriach INRIA i wciąż doskonalony. W
subiektywnej ocenie autorów niniejszych notatek, nie dorównuje on elegancji i
szlachetnej prostocie Octave. Na plus tego pakietu należy zaliczyć m.in.
znacznie bardziej rozbudowany zestaw funkcji podstawowych, na minus --- przede
wszystkim znacznie mniejszy stopień zgodności z MATLABem, a poza tym niewygodny
system pomocy oraz "toporną" (choć o dużym potencjale) grafikę.
 
Korzystając z dowolnego z omówionych powyżej pakietów, otrzymujemy:
* możliwość obliczania funkcji matematycznych (nawet dość egzotycznych), finansowych, analizy sygnałów, itp.;
* bardzo szeroki zakres nowoczesnych narzędzi umożliwiających wykonywanie podstawowych zadań numerycznych, takich jak: rozwiązywanie równań, obliczanie całek, itd.;
* efektowną wizualizację wyników w postaci wykresów dwu- i trójwymiarowych, a także funkcje do manipulacji obrazem i dźwiękiem;
* możliwość nieograniczonych rozszerzeń przy użyciu funkcji i skryptów tworzonych przez osoby trzecie (lub własnych).
Jeśli uwzględnić dodatki wielce ułatwiające życie, w tym rozbudowane funkcje
wejścia-wyjścia, to narzędzie robi się rzeczywiście intrygujące, jako środowisko
obliczeń numerycznych "dla każdego" (nie tylko dla profesjonalnego numeryka).
Stąd wielka popularność MATLABa w środowiskach inżynierskich, gdyż umożliwia
szybką implementację rozmaitych --- zwłaszcza testowych --- wersji algorytmu, np.
przeprowadzania skomplikowanej symulacji. Po zweryfikowaniu wyników można
ewentualnie rozpocząć implementację w docelowym środowisku (np. masywnie
równoległym komputerze, w Fortranie 95 z wykorzystaniem komercyjnych bibliotek i
specyficznymi optymalizacjami kodu), choć bardzo często wyniki uzyskane w
MATLABie w zupełności wystarczą.
 
Zarówno MATLAB, jak Octave i Scilab, opierają się w dużym zakresie na darmowych
--- acz profesjonalnych --- zewnętrznych bibliotekach numerycznych (BLAS, LAPACK, FFTW, itp.)
 
MATLAB jest wciąż niedościgniony jeśli chodzi o narzędzia programistyczne (np.
ma wbudowany --- działający <strong>na bieżąco</strong> w czasie edycji kodu źródłowego! --- debugger, a także kompilator funkcji JIT) oraz możliwości wizualizacji danych i wyników obliczeń. Jeśli zaniedbać koszt zakupu dodatkowych modułów, ma także najbogatszy zestaw funkcji numerycznych. W chwili pisania niniejszego tekstu, (stosunkowo tanie) wersje studenckie MATLABa, nie są oferowane w Polsce przez producenta, choć z drugiej
strony są często dostępne (nawet w wersji profesjonalnej) w komputerowych
laboratoriach akademickich w Polsce.
 
Zarówno w MATLABie, jak i w Octave, jest możliwe pisanie funkcji, które będą
prekompilowane, dając tym samym znaczące przyspieszenie działania. Najnowsze wersje MATLABa (od wersji 6.5) wykorzystują zresztą domyślnie kompilację JIT --- w trakcie pisania kodu źródłowego funkcji.
 
Wybór pomiędzy darmowymi klonami MATLABa: Octave i Scilabem, jest właściwie kwestią osobistych preferencji. W ninijeszym kursie będziemy posługiwać się
Octave, który jest bardzo wiernym klonem MATLABa, przez co daje zaskakująco wysoki stopień kompatybilności z programami napisanymi w MATLABie. To jest bardzo ważne w praktyce, ponieważ jest dostępnych wiele (nawet darmowych) pakietów numerycznych opracowanych właśnie w języku poleceń MATLABa. Umiejąc posługiwać się Octave, bez trudu można "przesiąść się" na MATLABa i korzystać z jego bogatszych możliwości, droga w przeciwną stronę też jest nietrudna.
Inną zaletą Octave jest swobodniejsza niż w MATLABie składnia.
 
Octave jest dołączany do większości popularnych dystrybucji Linuxa, najczęściej
jednak użytkownik musi samodzielnie doinstalować go z płytki instalacyjnej.
Ponadto, kody źródłowe najświeższej wersji Octave są dostępne na stronie
http://www.octave.org. Dodatkowo, pod http://octave.sourceforge.net znajdziemy pakiet rozszerzeń do Octave, pod nazwą Octave-forge. Wymaga on od użytkowników Linuxa samodzielnej (przebiegającej bezproblemowo) instalacji. Octave można także zainstalować pod Windows, korzystając z programu instalacyjnego dostępnego pod adresem internetowym http://octave.sourceforge.net: w Windowsowej wersji Octave, pakiet Octave-forge jest standardowo dołączony.
 
====Pierwsze kroki w Octave====
 
Octave uruchamiamy poleceniem <code style="color: #666">octave</code>,


ma się
<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;">
</div></div>
   
   
[przykry@bit MN] octave
GNU Octave, version 2.9.5 (i686-redhat-linux-gnu).
Copyright (C) 2006 John W. Eaton.
This is free software; see the source code for copying conditions.
There is ABSOLUTELY NO WARRANTY; not even for MERCHANTIBILITY or
FITNESS FOR A PARTICULAR PURPOSE.  For details, type `warranty'.
Additional information about Octave is available at http://www.octave.org.
Please contribute if you find this software useful.
For more information, visit http://www.octave.org/help-wanted.html
Report bugs to <bug@octave.org> (but first, please read
http://www.octave.org/bugs.html to learn how to write a helpful report).


Należy szczególną uwagę zwrócić na argumenty macierzowe funkcji w Fortranie,
octave:1>
gdyż bardzo łatwo tu o pomyłkę, którą potem trudno wychwycić. Aby niepotrzebnie
</div>
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
Naturalnie, Octave zachowuje wszystkie możliwości kalkulatora naukowego,
wymiaru <math>\displaystyle M\times N</math>:
wykonując podstawowe operacje arytmetyczne i obliczając wartości wielu funkcji
matematycznych; oto zapis takiej sesji Octave:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
<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;">
octave:1> 1900/2000
ans = 0.95000
octave:2> sin(pi)
ans =  1.2246e-16
octave:3> e^(i*pi)
ans = -1.0000e+00 + 1.2246e-16i
</div>
   
   
SUBROUTINE FILLMATRIX ( M, N, MATRIX )
Ostatnie dwa wyniki dają nam namacalny dowód, że obliczenia wykonywane są
INTEGER M,N
<strong>numerycznie</strong>, ze skończoną precyzją: w Octave niektóre tożsamości matematyczne
DOUBLE PRECISION MATRIX(M, N)
są spełnione <strong>jedynie w przybliżeniu</strong>, np. <math>\displaystyle \sin(\pi) \approx 0</math> oraz
<math>\displaystyle e^{i\pi} \approx -1</math>. Przy okazji widzimy, że Octave dysponuje podstawowymi
DO 10 I=1,M
stałymi matematycznymi (oczywiście, są to także wartości przybliżone!):
DO 20 J=1,N
<code style="color: #006">e <math>\displaystyle \approx</math> 2.71828182845905</code>,  <code style="color: #006">pi <math>\displaystyle \approx</math> 3.14159265358979</code>
MATRIX(I,J) = I+2*J
oraz jednostką urojoną <code style="color: #006">i</code> <math>\displaystyle = \sqrt{-1}</math>.
20 CONTINUE
 
10 CONTINUE
Octave ma dobrą dokumentację, dostępną w trakcie sesji
END
Octave; dla każdej funkcji, stałej itp.  można uzyskać jej dobry opis przy
</pre></div>
użyciu polecenia <code style="color: #006">help</code>.
 
Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz
dwuwymiarowa <math>\displaystyle n\times m</math>, o elementach rzeczywistych lub zespolonych,
<center><math>\displaystyle
\begin{pmatrix}
a_{11} & \cdots & a_{1m}\\
\vdots &        & \vdots\\
a_{n1} & \cdots & a_{nm}
\end{pmatrix} .
</math></center>
 
W szczególności, biorąc <math>\displaystyle m=1</math> albo <math>\displaystyle n=1</math>, dostajemy wektor (kolumnowy lub
wierszowy
<center><math>\displaystyle
\begin{pmatrix}
a_{1} \\
\vdots\\
a_{n}
\end{pmatrix} , \qquad\text{lub}\qquad
\begin{pmatrix}
a_{1} & \cdots & a_{m}
\end{pmatrix} .
</math></center>
 
Z kolei dla <math>\displaystyle m=n=1</math> mamy do czynienia ze "zwykłymi" liczbami rzeczywistymi lub
zespolonymi.
 
Octave umożliwia, za swoim pierwowzorem --- MATLABem, bardzo wygodną
pracę z macierzami.
 
Wprowadzanie macierzy do Octave jest bardzo intuicyjne i możemy zrobić to na
kilka sposobów, w zależności od potrzeb. Można robić to interakcyjnie, podając
elementy macierzy z linii komend, na przykład:
 
<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;">
   
   
Nawet osoby nie znające Fortranu domyślą się, że wynikiem działania naszej
octave:6> A = [2 -1 0; -1 3 -2; 2 2.71828 3.14] 
funkcji, np. dla <math>\displaystyle M=2</math>, <math>\displaystyle N=5</math>, będzie macierz
A =


  2.00000  -1.00000  0.00000
  -1.00000  3.00000  -2.00000
  2.00000  2.71828  3.14000
</div>
definiuje macierz kwadratową <math>\displaystyle 3\times 3</math> o elementach równych
<center><math>\displaystyle  
<center><math>\displaystyle  
\lstF{MATRIX} =
\begin{pmatrix}  
\begin{pmatrix}  
3 & 5 & 7 & 9 & 11 \\
2 & -1 & 0\\ -1 & 3 & -2\\ 2 & 2.71828 & 3.14
4 & 6 & 8 & 10 & 12
\end{pmatrix} .
\end{pmatrix}  
</math></center>
</math></center>


Naturalnie, możemy wywołać ją wprost z programu w C przy użyciu poprzednio
Tak
poznanej techniki i następującego kodu (tym razem prototyp funkcji
więc, macierz w Octave definiujemy przez podanie jej elementów. Elementy
<code>fillmatrix_</code> umieszczamy w osobnym pliku nagłówkowym <code style="color: #666">ffortran.h</code>,
wprowadzamy kolejno wierszami, elementy w wierszu oddzielamy spacjami (lub
gdzie mamy zamiar kolekcjonować prototypy w C lokalnie wykorzystywanych funkcji
ewentualnie przecinkami), natomiast kolejne wiersze oddzielamy średnikami.
fortranowskich):


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Macierz możemy także tworzyć stopniowo, wprowadzając jeden za drugim jej
kolejne elementy:
 
<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;">
   
   
/*Wykorzystanie funkcji fortranowskiej operującej na macierzy.
octave:3> B(1,1) = 4
Zwróćmy uwagę na sposób użycia argumentu macierzowego*/
B = 4
#include <stdio.h>
 
#include <stdlib.h>
octave:4> B(2,1) = 3 + B(1,1)
int fillmatrix_(int *, int *, double *);
B =
 
  4
  7
 
octave:5> B(3,2) = 28
B =


int main()
  4  0
{
  7  0
int MM, NN, i, j;
  0  28
double *A;
</div>
  MM = 2; NN = 5;
Pierwsza komenda określa <code style="color: #006">B</code> jako macierz <math>\displaystyle 1\times 1</math>, czyli zwykłą
liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia --
A = (double *)malloc(MM*NN*sizeof(double));
powoduje, że <code style="color: #006">B</code> zostaje rozszerzony do macierzy <math>\displaystyle 3\times 2</math>. Zauważmy, że
elementom <code style="color: #006">B</code> o nieokreślonych przez nas wartościach zostaje przypisana domyślna
wartość zero.


  fillmatrix_( &MM, &NN, A );
<nowiki>
W przypadku, gdy wypełniana macierz jest
duża, znacznie korzystniej jest prealokować macierz, ustawiając na samym
początku wszystkie elementy macierzy <strong>docelowego wymiaru</strong> (u nas to była
macierz <math>\displaystyle 3\times 2</math>) na zero; potem możemy już działać jak poprzednio:
</nowiki>


printf("\nKolejne elementy wektora A:\n\n")
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
  for ( i = 0; i < NN*MM ; i++ ){
printf("&#37;e\n", A[i] );
B = zeros(3,2)
}
B(1,1) = 3 + B(1,1)
B(2,1) = pi
B(3,2) = 28
</div>
Jeśli nie chcemy za każdym razem obserwować echa prowadzonych przez nas działań
na macierzy <code style="color: #006">B</code>, komendy powinniśmy kończyć średnikiem: średnik po
komendzie Octave blokuje echo wyniku jej wykonania. Tak więc, znacznie
wygodniej będzie nam napisać
 
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
B = zeros(3,2);
B(1,1) = 3 + B(1,1);
B(2,1) = pi;
B(3,2) = 28;
B
</div>
Ostatnie polecenie -- <code style="color: #006">B</code> <strong>bez</strong> średnika na końcu -- spowoduje
wypisanie zawartości <code style="color: #006">B</code> na ekran.


printf("\nWektor A zinterpretowany jako macierz:\n\n");  
Tak utworzoną macierz możemy zapisać do pliku, wykorzystując jeden z kilku
for ( j = 0 ; j < MM ; j++ )
formatów:  
{
* tekstowy
  for ( i = 0; i < NN ; i++ )
* binarny
  printf("&#37;e ", A[i*MM+j] );
* HDF5
printf("\n");
  }
Zapis w formacie tekstowym to po prostu zapis do formatu ASCII. Ze względu na późniejszą [[WDP_Reprezentacja_liczb|konwersję z zapisu w
postaci dziesiętnej do dwójkowej]], wiąże się z drobnymi
niedokładnościami w odczycie.
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
save macierzb.dat B
</div>
W wyniku dostajemy
<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;">
# Created by Octave 2.9.5, Mon Jul 31 17:15:25 2006 CEST <przykry@bit>
# name: B
# type: matrix
# rows: 3
# columns: 2
3 0
3.14159265358979 0
0 28
</div>
Zapis binarny to prostu zrzut stanu pamięci
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
save -binary  macierzb.dat B
</div>
Format [http://hdf.ncsa.uiuc.edu/HDF5  HDF5] to format gwarantujący pełną przenośność danych pomiędzy różnymi
architekturami, akceptowany przez wiele zewnętrznych aplikacji, m.in.
[http://www.opendx.org  OpenDX].


  free( A );
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
save -hdf5  macierzb.dat B
</div>
Dane z pliku wczytujemy z powrotem poleceniem


  return(0);
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
}
</pre></div>
load macierzb.dat
</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ę
Sesję Octave kończymy poleceniem <code style="color: #006">quit</code>.
jednowymiarową</strong> odpowiedniej wielkości.
 
====Notacja dwukropkowa Molera====
 
Do istniejącej macierzy możemy odwoływać się tradycyjnie, czyli do pojedynczych
elementów, np. <code style="color: #006">alfa = D(2,2)</code>, lub do jej większych bloków, używając popularnej tzw. <strong>notacji dwukropkowej</strong> Molera.  


==BLAS, LAPACK i ATLAS==
[[Image:MNcolon-matrix-1-color.png|thumb|550px|center|Wybór bloku w macierzy <code style="color: #006">D</code> przy użyciu notacji dwukropkowej]]


W zadaniach dotyczących macierzy gęstych, warto skorzystać z klasycznego tandemu
Jest ona
bibliotek: [http://www.netlib.org/blas  BLAS] ''Basic Linear Algebra Subprograms''
szalenie intuicyjna, a mianowicie: pisząc na przykład <code style="color: #006">v = D(2:5,2)</code>
oraz [http://www.netlib.org/blas  LAPACK] ''Linear Algebra PACKage''. Dla macierzy
definiujemy (wektor) <math>\displaystyle v</math>, który zawiera wiersze macierzy <math>\displaystyle D</math> od 2 do 5 wybrane z
rozrzedzonych (zawierających wiele elementów zerowych) warto zastosować bardziej
drugiej kolumny. Podobnie, pisząc <code style="color: #006">W = D(2:3,5:7)</code> definiujemy macierz
wyspecjalizowane biblioteki. Aby wykorzystać do maksimum moc oferowaną przez te
<math>\displaystyle W</math> wymiaru
biblioteki (w połączeniu z naszym komputerem) warto skorzystać z optymalizowanej
<math>\displaystyle 2\times 3</math>, o elementach, które zostały wybrane z przecięcia wierszy od 2 do 3 z
wersji BLASów i LAPACKa, czyli z [http://math-atlas.sourceforge.net  ATLAS]a,  . Istnieje inna
kolumnami od 5 do 7 macierzy <math>\displaystyle D</math>.
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!]]
[[Image:MNcolon-matrix-2-color.png|thumb|550px|center|Wybór zestawu kolumn w macierzy <code style="color: #006">D</code>]]


BLAS i LAPACK są często wykorzystywane przez inne biblioteki numeryczne,
Dodatkowo, aby odwołać się do całego 4. wiersza (odpowiednio: 5. kolumny)
na nich opierają się również funkcje macierzowe w Octave i MATLABie.
macierzy <math>\displaystyle D</math>, można użyć skrótowej notacji <code style="color: #006">D(4,:)</code> (odpowiednio:
Optymalizowane biblioteki BLAS i LAPACK dla swoich architektur oferują
<code style="color: #006">D(:,5)</code>).  
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
Odwołanie się do całej macierzy jest także możliwe (przez użycie jej nazwy, np.
algebry liniowej: skalarami, wektorami i macierzami. Obsługiwane są obiekty
<code style="color: #006">A = 2*D</code> lub <code style="color: #006">G = log(abs(D))</code>).
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:  
Notację dwukropkową można także wykorzystać do wygenerowania samodzielnych zbiorów indeksó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;
<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;">
* 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.
octave:1> N = 10;
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.
octave:2> idx = 1:N
idx =
  1  2  3  4  5  6  7  8  9 10


Większość użytkowników BLAS nie będzie jednak miała potrzeby pisania własnych
octave:3> idx2 = 1:2:N
algorytmów blokowych, gdyż funkcje rozwiązujące podstawowe zadania numerycznej
idx2 =
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.
  1  3  5  7  9


Nazwy procedur BLASów i
octave:4> nidx = N:-1:1
LAPACKa są cokolwiek enigmatyczne na pierwszy rzut oka, ale w istocie dość
nidx =
łatwo je odgadywać. Każda nazwa składa się z kilku części, najczęściej jest
  10  9  8  7  6  5  4  3  2  1
postaci <code style="color: #903">PRRFF</code>, gdzie
</div>
   
   
;
i dlatego pętle, które w C zapisywalibyśmy
:  <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;


;
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;">
: <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;
for(i=0; i<=N; i++)
{
...instrukcje...
}
for(i=N; i>=1; i--)
{
...instrukcje...
}
</div>
w Octave zapiszemy


;
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
<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)
for i=0:N
...instrukcje...
end
for i=N:-1:1
...instrukcje...
end
</div>
   
   
Jeśli jednak <strong>nie możemy zgadnąć</strong>, jaka jest nazwa procedury BLAS/LAPACKa,
Za chwilę przekonamy się, jak można w ogóle pozbyć się potrzeby stosowania większości pętli w kodach Octave i MATLABa.
która byłaby nam potrzebna,
 
najlepiej przejrzeć (przeszukać) strony internetowe tych pakietów w serwisie
====Wektoryzacja w Octave====
[http://www.netlib.org  Netlib].
 
Ponieważ podstawowymi obiektami w Octave są wektory i macierze, predefiniowane
operacje matematyczne wykonują się od razu na całej macierzy. Bez żadnej
przesady możena stwierdzić, że umiejętność  <strong>wektoryzacji</strong> i
<strong>blokowania</strong> algorytmów jest podstawą pisania efektywnych implementacji
algorytmów w Octave.


Zestawienie najczęściej wykorzystywanych procedur BLAS i LAPACKa przedstawiamy
Zobaczmy kilka prostych przykładów zawartych w tabeli poniżej. W
poniżej. Każda z tych procedur ma swój wariant "ekspercki", np. dla
pierwszej kolumnie tabeli, dane zadanie zaimplementujemy w Octave przy użyciu
rozwiązywania układów równań metodą eliminacji Gaussa można skorzystać z
pętli (zwróćmy przy okazji uwagę na to, jak programuje się pętle w Octave, one
osobnych procedur generujących rozkład LU oraz z innych, rozwiązujących układy
nie są zupełnie bezużyteczne...). W drugiej kolumnie zobaczymy, jak to samo
trójkątne.
zadanie można wykonać korzystając z operatorów lub funkcji macierzowych.


{| border=1
{| border=1
Linia 615: Linia 860:
|-  
|-  
|  
|  
 
  ||  Tradycyjny kod w Octave, używający pętli ||  Efektywny kod wektorowy (macierzowy) w Octave
Zadanie algebry liniowej ||  Nazwa procedury BLAS/LAPACK
|-
|-
|  
|  


mnożenie wektora przez macierz ||  <code style="color: #903">DGEMV</code>
Alg 1 ||  
|-
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
| mnożenie macierzy przez macierz ||  <code style="color: #903">DGEMM</code>
s = 0;
for i = 1:size(x,1)
s = s + abs(x(i));
end
</div>  
  ||  
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
s = sum(abs(x));
</div>  
|-
|-
|  
|  
rozwiązywanie układu równań ||  <code style="color: #903">DGESV</code>
 
|-
Alg 2 ||  
| rozkład LU (w miejscu) ||  <code style="color: #903">DGETRF</code>
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
|-
| rozwiązywanie układu z rozkładem uzyskanym z <code style="color: #903">DGETRF</code>  ||  <code style="color: #903">DGETRS</code>
N = 500; h = (b-a)/(N-1);
for i = 1:N
x(i) = a + (i-1)*h;
y(i) = sin(x(i));
end
plot(x,y);
</div>  
||  
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
N = 500;
x = linspace(a,b,N);
y = sin(x);
plot(x,y);
</div>  
|-
|-
|  
|  
rozwiązywanie układu z macierzą symetryczną ||  <code style="color: #903">DSYSV</code>
Alg 3a ||  
|-
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
| rozkład LDL<math>\displaystyle ^T</math> macierzy symetrycznej (w miejscu) ||  <code style="color: #903">DSYTRF</code>
|-
for i = 1:size(C,1),
| rozwiązywanie układu z rozkładem uzyskanym z <code style="color: #903">DSYTRF</code>  ||  <code style="color: #903">DSYTRS</code>
for j = 1:size(C,2),
for k = 1:size(A,2),
C(i,j) = C(i,j) + A(i,k)*B(k,j);
end
end
end
</div>  
  ||  
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
C = C + A*B;
</div>  
|-
|-
|  
|  
rozwiązywanie układu z macierzą pasmową ||  <code style="color: #903">DGBSV</code>
 
|-
Alg 3b ||  
| rozkład LU macierzy pasmowej (w miejscu) ||  <code style="color: #903">DGBTRF</code>
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
|-
| rozwiązywanie układu z rozkładem uzyskanym  z <code style="color: #903">DGBTRF</code>  ||  <code style="color: #903">DGBTRS</code>
for i = 1:size(C,1),
|-
for j = 1:size(C,2),
|
C(i,j) = C(i,j) + A(i,:)*B(:,j);
zagadnienie własne  ||  <code style="color: #903">DGEEV</code>
end
end
</div>  
  ||  
  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;">
C = C + A*B;
</div>  
|-
|-
|  
|  
Linia 652: Linia 946:
|}
|}


<!--
Zwróćmy uwagę na to, że kod wektorowy Octave oraz, w jeszcze większym stopniu,
 
kod macierzowy jest znacznie bardziej elegancki i czytelniejszy od
====Mnożenie macierz-wektor w BLAS====
tradycyjnego. Widać to dobrze na wszystkich powyższych przykładach.


Zacznijmy od prostej procedury BLAS Level 2, jaką jest mnożenie macierzy przez
Pierwszy przykład pokazuje też, że kod macierzowy jest elastyczniejszy: albowiem
wektor. Wykorzystamy tzw. wzorcową implementację BLASów (niezoptymalizowaną)
obliczy sumę modułów wszystkich elementów <code style="color: #006">x</code> nie tylko gdy <code style="color: #006">x</code>
dostarczaną z dystrybucją np. Red Hat Linux. Jest to  biblioteka funkcji
jest wektorem (co musieliśmy milcząco założyć w kodzie w lewej kolumnie), ale
fortranowskich.
tak samo dobrze zadziała, gdy <code style="color: #006">x</code> jest macierzą!


Naszym zadaniem jest wykonanie operacji
Szczególnie wiele na czytelności i wygodzie zyskujemy w drugim przykładzie,
<center><math>\displaystyle  
gdzie najpierw funkcja <code style="color: #006">linspace</code> pozwala nam uniknąć żmudnego
y \leftarrow  \alpha A x + y,
wyznaczania <math>\displaystyle N</math> równoodległych węzłów <math>\displaystyle x_i</math> w odcinku <math>\displaystyle [a,b]</math>, a następnie
</math></center>
funkcja <code style="color: #006">sin</code> zaaplikowana do całego wektora <math>\displaystyle x</math> daje wartość sinusa w
zadanych przez nas węzłach.


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>
Kod wektorowy lub (zwłaszcza) macierzowy jest też znacznie szybszy. Spójrzmy
współrzędnych.
teraz na przykłady: (3a) i (3b), które pokażą nam prawdziwą moc funkcji
 
macierzowych, unikających wielokrotnie zagnieżdżonych pętli. Oba dotyczą
To zadanie realizuje procedura BLASów o nazwie
operacji mnożenia dwóch macierzy. Przykład (3a) w wersji z potrójną pętlą
<code style="color: #903">DGEMV</code>. W rzeczywistości ta procedura wykonuje ogólniejsze zadanie
<code style="color: #006">for</code> naśladuje sposób programowania znany nam z C lub Pascala,
wyznaczania wektora
natomiast przykład (3b) zdaje się być napisany odrobinę w duchu wektorowym
 
(brak trzeciej, wewnętrznej pętli, zastąpionej operacją wektorową:  iloczynem
<center><math>\displaystyle  
skalarnym <math>\displaystyle i</math>-tego wiersza <math>\displaystyle A</math> i <math>\displaystyle j</math>-tej kolumny <math>\displaystyle B</math>). Poniżej porównanie
y \leftarrow  \alpha B x + \beta y,
czasów działania tych trzech implementacji w przypadku macierzy <math>\displaystyle 64\times 64</math>
</math></center>
(czasy dla PC z procesorem Celeron 1GHz):
 
* Dla pętli postaci <code style="color: #006">C(i,j)=C(i,j)+A(i,k)*B(k,j)</code> uzyskano czas 21.6s,
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
* Dla pętli postaci  <code style="color: #006">C(i,j)=C(i,j)+A(i,:)*B(:,j)</code> --- 0.371s
razem argumentem macierzowym, jaki przekazujemy <code style="color: #903">DGEMV</code>, jest wyjściowa
* Dla pętli postaci <code style="color: #006">C=C+A*B</code> kod działał jedynie 0.00288s!
macierz <math>\displaystyle A</math>).
 
Jak wiemy, że jest możliwe bezpośrednie wykorzystanie
biblioteki fortranowskiej w programie w C, jednak musimy pamiętać, iż macierze z
jakich ona skorzysta muszą być ułożone <strong>kolumnami</strong> w jednolitym bloku
pamięci.
 
Bazując na opisie procedury <code style="color: #903">DGEMV</code> ze
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>
   
   
int dgemv_( char* TRANS,  
Widzimy, jak beznadziejnie wolny jest kod oparty na trzech zagnieżdżonych
int* M,
pętlach: jest on kilka tysięcy razy wolniejszy od implementacji macierzowej
int* N,
<code style="color: #006">C = C + A*B</code>. Po wektoryzacji wewnętrznej pętli, program doznaje
double* ALPHA,
kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie
double* A, 
wolniejszy od kodu macierzowego!
int* LDA, 
double* X, 
int* INCX,
double* BETA,
double* Y,
int* INCY );
</pre></div>
 
Dla własnej wygody, a także dla przyszłego wykorzystania, umieścimy ten
prototyp, razem z innymi przydatnymi drobiazgami (m.in. makro <code>IJ</code>
dające wygodny dostęp do macierzy niezależny od jej wewnętrznej reprezentacji, a
także zmienne całkowite
<code>static int BLASONE = 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
=Literatura=
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>
Materiały zgromadzone w kolejnych wykładach są przewodnikiem po zagadnieniach w nich omawianych. Prawdziwe studia wymagają wszelako <strong>studiowania</strong> --- także dobrej literatury fachowej! Dlatego pod koniec każdego wykładu umieszczamy informację o tym, gdzie szukać pogłębionych informacji na temat omawianego na wykładzie materiału.  
#include <stdio.h>
#include "blaslapack.h"


double* mmread(char *filename, int* N, int* M );
Naszym podstawowym źródłem będzie nowoczesny podręcznik
#<span style="font-variant:small-caps">D. Kincaid, W. Cheney</span>, <cite>Analiza numeryczna</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa, 2006.


int main()
Oprócz niego, w języku polskim było wydanych kilka innych podręczników i monografii (niestety, większość z nich dość dawno temu)
{
#<span style="font-variant:small-caps">M. Dryja, J. i M. Jankowscy</span>, <cite>Przegląd metod i algorytmów numerycznych</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa, 1988,
int N, M, i, j;
#<span style="font-variant:small-caps">A. Bjorck, G. Dahlquist</span>, <cite>Metody numeryczne</cite>, PWN, Warszawa, XXXX,
double *A, *x, *y;
#<span style="font-variant:small-caps">Stoer, Bulirsch</span>, <cite>Wstęp do analizy numerycznej</cite>, PWN, Warszawa, XXXX,
#<span style="font-variant:small-caps">A.Kiełbasiński, H. Schwetlick</span>, <cite>Numeryczna algebra liniowa</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa, 1992.
/* wczytujemy macierz z pliku w formacie MatrixMarket */
#<span style="font-variant:small-caps">A. Ralston</span>, <cite>XXXX</cite>, PWN, Warszawa, XXXX,
/* macierz jest reprezentowana w formacie kolumnowym  */
#<span style="font-variant:small-caps">Fortuna, Macukow, </span>, <cite></cite>,  
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 */
 
{
char TRANS = 'N'; double ALPHA = 5.0, BETA = 0.0;
dgemv_(&TRANS, &N, &M, &ALPHA, A, &N, x, &BLASONE,
                        &BETA, y, &BLASONE );


}
Do tego warto wspomnieć o kilku wartościowych podręcznikach i monografiach w języku angielskim
#<span style="font-variant:small-caps">A. Quarteroni, Valli, Saleri</span>, <cite></cite>, ,
/* wydruk wyniku */
#<span style="font-variant:small-caps">P. Deuflhard, A. Hohmann</span>, <cite>Numerical Analysis in Modern Scientific Computing. An Introduction</cite>, Springer, 2003, ISBN 0-387-95410-4,
for (i = 1; i <= M; i++)
#<span style="font-variant:small-caps">K. Atkinson</span>, <cite>An Introduction to Numerical Analysis</cite>, Wiley, 1989,
printf("&#37;E\n",y[IJ(i,1,M)]);
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
return(0);
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
}
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
</pre></div>
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
Zwróćmy uwagę na wykorzystanie "stałej" <code>BLASONE</code>, równej 1,
predefiniowanej w pliku <code style="color: #666">blaslapack.h</code>.


-->
Zachęcamy gorąco do zapoznania się z nimi i sztuką obliczeń numerycznych, gdyż jej prawdziwy urok, niuanse i tajemnice --- podobnie jak w przypadku każdej innej ''sztuki'' --- może docenić jedynie ''wyrobiony'' odbiorca.
Programy korzystające z BLASów i LAPACKa kompilujemy
standardowo, pamiętając o dołączeniu na etapie linkowania używanych przez nas
bibliotek:


<div class="code" style="background-color:#e8e8e8; padding:1em; margin-top,margin-bottom: 1em; margin-left,margin-right:2em;"><pre>
Jeśli chodzi o programowanie w Octave lub w MATLABie, warto skorzystać z  [http://www.octave.org/manual  internetowego podręcznika Octave] lub [ co jeszcze].
   
gcc -o testblas testblas.c -llapack -lblas -lgfortran -lm
</pre></div>

Wersja z 15:54, 27 wrz 2006



\fbox{\beginminipage {0.8\columnwidth}Do każdego wykładu podać na początku zestaw rozdziałów z podręcznika, które należy opanować, a na końcu --- kilka pozycji literatury uzupełniającej. W rozwiązaniach ćwiczeń też odsyłać do podręczników.\endminipage }

Wprowadzenie do metod numerycznych

Metody numeryczne to dziedzina wiedzy zajmująca się problemami obliczeniowymi i konstrukcją algorytmów rozwiązywania zadań matematycznych. Najczęściej, zadania obliczeniowe postawione są w dziedzinie rzeczywistej (lub zespolonej) i dlatego mówimy o zadaniach obliczeniowych matematyki ciągłej (w odróżnieniu od matematyki dyskretnej).

Zadania metod numerycznych

Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw

  • określić dane problemu i cel obliczeń, czyli dokładnie

sformułować zadanie w języku matematyki,

  • określić środki obliczeniowe dzięki którym chcemy osiągnąć cel,
  • dla analizy zadania i sposobów jego rozwiązania wygodnie jest zdefiniować

klasę rozpatrywanych danych oraz model obliczeniowy w obrębie którego będą działać nasze algorytmy.

Wbrew dość powszechnej opinii nie jest prawdą, że głównym przedmiotem metod numerycznych jest badanie wpływu błędów zaokrągleń na wynik. Raczej, głównym celem metod numerycznych jest konstrukcja optymalnych (w jasno określonym sensie, np. pod względem wymaganej liczby operacji, lub pod względem ilości niezbędnej informacji, czy też pod względem dokładności uzyskiwanego wyniku) algorytmów rozwiązywania konkretnych zadań matematycznych.

Uwaga

Nasz przedmiot ma różne wcielenia i z tego powodu czasem nosi inne nazwy, w zależności od tego, na jaki aspekt metod obliczeniowych jest położony największy nacisk.

metody numeryczne
--- główny nacisk idzie na aspekty algorytmiczne;
analiza numeryczna
--- przede wszystkim badanie właściwości algorytmów, ich optymalności oraz wpływu arytmetyki zmiennopozycyjnej na jakość uzyskanych wyników;
matematyka obliczeniowa
--- głównie teoretyczna analiza możliwości taniej i dokładnej aproksymacji rozwiązań zadań matematycznych;
obliczenia naukowe
--- nacisk na praktyczne zastosowania metod numerycznych, symulacje, realizacje na komputerach o dużej mocy obliczeniowej.

Oczywiście, granice podziału nie są ostre i najczęściej typowy wykład z tego przedmiotu stara się pokazać pełne spektrum zagadnień z nim związanych. Tak będzie również i w naszym przypadku.

Model obliczeniowy

Tworząc i analizując algorytmy, jakie będą pojawiać w naszym wykładzie, będziemy posługiwać się pewnym uproszczonym modelem obliczeń, dzięki czemu będziemy mogli skoncentrować się na esencji algorytmu, bez detali implementacyjnych --- zostawiając je na inną okazję (dobra implementacja konkretnego algorytmu może być sama w sobie interesującym wyzwaniem programistycznym; często bywa, że dobre implementacje, nawet prostych algorytmów numerycznych, są mało czytelne).

Aby zdefiniować nasz model obliczeniowy, posłużymy się pojęciem programu. Zastosujemy przy tym notację podobną do tej z języka programowania C.

Program składa się z deklaracji, czyli opisu obiektów, których będziemy używać, oraz z poleceń (instrukcji), czyli opisu akcji, które będziemy wykonywać.

Dostępnymi obiektami są stałe i zmienne typu całkowitego (int), rzeczywistego (float i double). Typ logiczny symulujemy tak jak w C wartościami zero-jedynkowymi typu całkowitego.

Zmienne jednego typu mogą być grupowane w wektory albo tablice.

Widzimy więc, że podstawowe algorytmy numeryczne będą bazować na mało skomplikowanych typach danych. Również nieskomplikowane będą instrukcje naszego modelowego języka. Dążenie do prostoty (ale nie za wszelką cenę) jest charakterystyczne dla numeryki. Typowe jest zastępowanie skomplikowanych tablic rekordów prostszymi macierzami, eleganckich rekurencji --- zwykłymi pętlami działającymi na danych w miejscu. Jedną z myśli przewodnich numeryki jest bowiem szybkość i ta rezygnacja z barokowych konstrukcji językowych jest tu analogiczna do sposobu konstrukcji architektury procesora RISC, pod hasłem efekywność przez prostotę.

(Na pewno zastanawiasz się teraz, jaka jest druga myśl przewodnia numeryki. To dokładność.)

Podstawienie

Najprostsza z instrukcji, bez której nie można się obejść:

 z = 𝒲;

Tutaj z jest zmienną, a 𝒲 jest wyrażeniem o wartościach tego samego typu co z. Jest to polecenie proste.

Wyrażeniem jest pojedyncza stała lub zmienna, albo złożenie skończonej liczby operacji elementarnych na wyrażeniach. Operacje elementarne to:

arytmetyczno--arytmetyczne
xx, (x,y)x+y,

(x,y)xy, (x,y)x*y, (x,y)x/y,y0, gdzie x,y są stałymi lub zmiennymi liczbowymi,

arytmetyczno--logiczne
(x,y)x<y,

(x,y)xy, (x,y)x=y, (x,y)xy, gdzie x,y są stałymi lub zmiennymi liczbowymi,

logiczno--logiczne
pp,

(p,q)pq, (p,q)pq, gdzie p,q są stałymi lub zmiennymi logicznymi.

Dla niektórych zadań wygodnie jest (a czasem konieczne) uzupełnić ten zbiór o dodatkowe operacje, takie jak obliczanie wartości niektórych standardowych funkcji matematycznych (,cos(),sin(),exp(),log(), itp.), czy nawet funkcji bardziej skomplikowanych. Na przykład, zastosowanie "szkolnych" wzorów na obliczanie pierwiatków równania kwadratowego byłoby niemożliwe, gdyby pierwiastkowanie było niemożliwe.

Uwaga

Należy pamiętać, że w praktyce funkcje standardowe (o ile są dopuszczalne) są obliczane używając czterech podstawowych operacji arytmetycznych. Dokładniej, jednostka arytmetyczna procesora potrafi wykonywać jedynie operacje +,,×,÷, przy czym dzielenie zajmuje kilka razy więcej czasu niż pozostałe operacje arytmetyczne. Niektóre procesory (np. Itanium 2) są w stanie wykonywać niejako jednocześnie operację dodawania i mnożenia (tzw. FMADD, fused multiply and add). Praktycznie wszystkie współczesne procesory mają także wbudowany koprocesor matematyczny, realizujący asemblerowe polecenia wyznaczenia wartości standardowych funkcji matematycznych (,cos(),sin(),exp(),log(), itp.), jednak wykonanie takiej instrukcji wymaga mniej więcej kilkadziesiąt razy więcej czasu niż wykonanie operacji dodawania.

Uwaga

Niestety, aby nasz model obliczeniowy wiernie odpowiadał rzeczywistości, musimy w nim uwzględnić fakt, że działania matematyczne (i, tym bardziej, obliczanie wartości funkcji matematycznych) nie są wykonywane dokładnie. Czasem uwzględnienie tego faktu wiąże się ze znaczącym wzrostem komplikacji analizy algorytmu i dlatego "w pierwszym przybliżeniu" często pomija się to ograniczenie przyjmując model w którym wszystkie (lub prawie wszystkie) działania arytmetyczne wykonują się dokładnie. Wiedza o tym, kiedy i jak zrobić to tak, by wciąż wyciągać prawidłowe wnioski odnośnie faktycznej realizacji algorytmów w obecności błędów zaokrągleń jest częścią sztuki i wymaga intuicji numerycznej, popartej doświadczeniem.

Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane.

Warunkowe

if(𝒲) 𝒜1; else 𝒜2;

gdzie 𝒲 jest wyrażeniem o wartościach całkowitych (0 odpowiada logicznemu fałszowi, inne wartości --- logicznej prawdzie), a 𝒜1 i 𝒜2 są poleceniami, przy czym dopuszczamy polecenia puste.

Powtarzane

while(𝒲) 𝒜;

gdzie W jest wyrażeniem o wartościach logicznych, a 𝒜 jest poleceniem.

Kombinowane

{ 𝒜1;𝒜2;𝒜n; }

gdzie 𝒜j są poleceniami.

Na podstawie tych trzech poleceń można tworzyć inne, takie jak pętle for(), czy switch(), itd.

Mamy też dwa szczególne polecenia, które odpowiadają za "wejście" i "wyjście".

Wprowadzanie danych

 𝒩(x,t);

gdzie x jest zmienną rzeczywistą, a t "adresem" pewnego funkcjonału Parser nie mógł rozpoznać (nieznana funkcja „\toR”): {\displaystyle \displaystyle L:F\toR} należącym to pewnego zbioru T. W wyniku wykonania tego polecenia w zmiennej x zostaje umieszczona wartość Lt(f).

Polecenie to pozwala zdobyć informację o danej f. Jeśli F=Rn to zwykle mamy T={1,2,,n} i Li(f)=fi, co w praktyce odpowiada wczytaniu i-tej współrzędnej wektora danych. W szczególności, ciąg poleceń 𝒩(x[i],i), i=1,2,,n, pozwala uzyskać pełną informację o f. Jeśli zaś F jest pewną klasą funkcji Parser nie mógł rozpoznać (nieznana funkcja „\toR”): {\displaystyle \displaystyle f:[a,b]\toR} , to możemy mieć np. T=[a,b] i Lt(f)=f(t). W tym przypadku, wykonanie polecenia 𝒩(x,t) odpowiada w praktyce skorzystaniu ze specjalnej procedury (albo urządzenia zewnętrznego) obliczającej (mierzącego) wartość funkcji f w punkcie t.

Wyprowadzanie wyników

 𝒪𝒰𝒯(𝒲);

gdzie 𝒲 jest wyrażeniem o wartościach rzeczywistych. Polecenie to pozwala "wskazać" kolejną współrzędną wyniku.

Zakładamy, że na początku procesu obliczeniowego wartości wszystkich zmiennych są nieokreślone, oraz że dla dowolnych danych wykonanie programu wymaga wykonania skończonej liczby operacji elementarnych. Wynikiem obliczeń jest skończony ciąg liczb rzeczywistych (albo puste), którego kolejne współrzędne pokazywane są poleceniem 𝒪𝒰𝒯.

Środowisko obliczeniowe

Ze względu na swój utylitarny charakter, w metodach numerycznych niemal równie ważna jak dobór optymalnego algorytmu jest jego efektywna implementacja na konkretnej architekturze.

W praktyce mamy dwie możliwości:

  • wykorzystanie standardowych języków programowania (C, Fortran, być może ze wstawkami w asemblerze) oraz wyspecjalizowanych bibliotek
  • użycie gotowego środowiska obliczeń numerycznych będącego wygodnym interfejsem do specjalizowanych bibliotek numerycznych

Zaleta pierwszego podejścia to (zazwyczaj) szybko działający kod wynikowy, ale kosztem długotrwałego i żmudnego programowania. W drugim przypadku jest na odwrót: developerka i testowanie --- wyjątkowo ważne w przypadku programu numerycznego --- postępują bardzo szybko, ale czasem kosztem ogólnej efektywności uzyskanego produktu.

Języki programowania: C i Fortran

Programy numeryczne (a przynajmniej ich jądra obliczeniowe) są zazwyczaj niezbyt wymagające jeśli chodzi o struktury danych, co więcej, prostota struktur danych szybko rewanżuje się efektywniejszym kodem. Dlatego, trawestując Einsteina, w dobrym programie numerycznym należy

używać tak prostych struktur danych, jak to możliwe (ale nie prostszych!...)

Językami opartymi na prostych konstrukcjach programistycznych są: Fortran i C. Dlatego właśnie są to języki dominujące współcześnie pisane programy numeryczne. O ile w przeszłości hegemonia Fortranu była nie do podważenia, o tyle teraz coraz więcej oprogramowania numerycznego powstaje w C.

W naszym wykładzie wybieramy C ze względu na jego uniwersalność, doskonałą przenośność i całkiem dojrzałe kompilatory. Dodajmy, że funkcje w C można mieszać np. z gotowymi bibliotekami napisanymi w Fortranie. Fortran, język o bardzo długiej tradycji, wciąż żywy i mający grono wiernych fanów, jest nadal wybierany przez numeryków na całym świecie między innymi ze względu na jego dopasowanie do zadań obliczeniowych (właśnie w tym celu powstał), a także ze względu na doskonałe kompilatory dostępne na superkomputerach, będące efektem wieloletniej ewolucji i coraz lepszego nie tylko dopasowania kompilatora do spotykanych konstrukcji językowych, ale także na odwrót --- coraz lepszego zrozumienia programistów, jak pisać programy, by wycisnąć jak najwięcej z kompilatora.

Inne popularne języki: Java, Pascal (ten język, zdaje się, jest popularny już tylko w obrębie wydziału MIM UW...), VisualBasic i inne, nie są zbyt odpowiednie dla obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala: real nie jest zgodny z powszechnym standardem IEEE 754. Jednak, ze względu na coraz większą komplikację kodów numerycznych służących np. do prowadzenia zaawansowanych symulacji metodą elementu skończonego, coraz więcej kodów wykorzystuje możliwości obiektowych języków C++ i Fortran90.

W przykładach będziemy najczęściej odnosić się do architektury x86, tzn. 32-bitowej IA-32 procesorów firmy Intel i AMD, najczęściej spotykanej w obecnie używanych komputerach. Należy jednak pamiętać, że obecnie następuje przejście na architekturę 64-bitową. Ze względu jednak na brak pewności co do ostatecznie przyjętych standardów w tym obszarze, ograniczymy się do procesorów 32-bitowych. W przykładach będziemy korzystać z kompilatora GCC, który jest omówiony w wykładzie Środowisko programisty.

Prosty program numeryczny w C

Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) N-tą sumę częściową szeregu harmonicznego

x=i=1N1i.

Przyjmijmy, że parametr N będzie miał wartość równą 2006. W pierwszym odruchu, prawie każdy początkujący student pisze program w rodzaju:

  1. include <stdio.h>
  2. define N 2006

int main(void) { float x; unsigned int i;

x = 0.0; for(i = 1; i <= N; i++) x = x + 1/i; printf("Wartość sumy x = 1 + 1/2 + ... + 1/%d jest równa %g\n", N, x);

return(0); }

Sęk w tym, że ten program nie działa! To znaczy: owszem, kompiluje się i uruchamia, ale twierdzi uparcie, że nasza suma wynosi... 1.

Winę za to ponosi linijka

x = x + 1/i;

w której wykonujemy dzielenie 1/i. Obie liczby są typu int i dlatego, zgodnie z regułami C, wynik ich dzielenia także jest całkowity: dostajemy część całkowitą z dzielenia tych dwóch liczb, czyli, gdy tylko i>1, po prostu zero.

Prawidłowy program musi więc wymusić potraktowanie choć jednej z tych liczb jako liczby zmiennoprzecinkowej, co najprościej uzyskać albo przez

x = x + 1.0/i;

albo bardziej uniwersalnie, rzutując choć jedną z liczb na odpowiedni typ:

x = x + 1/((float) i);

Poprawny kod miałby więc postać

  1. include <stdio.h>
  2. define N 2006

int main(void) { float x; unsigned int i;

x = 0.0; for(i = 1; i <= N; i++) x = x + 1.0/i; printf("Wartość sumy x = 1 + 1/2 + ... + 1/%d jest równa %g\n", N, x);

return(0); }

Typy numeryczne w C

W języku C mamy dostępnych sześć typów numerycznych:

  • stałoprzecinkowe, dla reprezentacji liczb całkowitych
    • int oraz long int. W realizacji GCC na komputery klasy PC, oba typy: int i long int są identyczne (32-bitowe) i ich zakres wynosi około 2.1109+2.1109. Typ int i jemu pokrewne odnoszą się do liczb całkowitych ze znakiem (dodatnich lub ujemnych). Ich warianty bez znaku: unsigned int, itp. odnoszą się do liczb bez znaku (nieujemnych), dlatego np. zakresem unsigned int będzie w przybliżeniu 0+4.2109.
    • long long int (64-bitowy) o zakresie w przybliżeniu 9.21018+9.21018.
  • zmiennopprzecinkowe, dla reprezentacji liczb rzeczywistych
    • float, pojedynczej precyzji, 32-bitowy, gwarantuje precyzję około 107 i zakres liczb reprezentowalnych w przybliżeniu 10381038;
    • double, podwójnej precyzji, 64-bitowy, ma precyzję na poziomie 1016 przy orientacyjnym zakresie 1030810308;
    • long double, rozszerzonej podwójnej precyzji, na pecetach 80-bitowy, ale w pamięci zajmuje on 12 bajtów) o precyzji rzędu 1020 i odpowiadający standardowi double extended IEEE 754.

Powyższe typy zmiennoprzecinkowe w realizacji na PC odpowiadają standardowi IEEE 754. Standardowo, operacje arytmetyczne na obu typach float i double są tak samo pracochłonne, gdyż wszystkie obliczenia w C wykonywane są z maksymalną dostępną precyzją (czyli, na procesorach architektury IA-32 Intela i AMD: w precyzji oferowanej przez typ long double), a następnie dopiero wynik zapisywany do zmiennej reprezentowany jest w stosownym typie . Jednakże typ pojedynczej precyzji float oferuje znacznie większe możliwości optymalizacji uzyskanego kodu a ponadto, obiekty typu float zajmują dwukrotnie mniej miejsca w pamięci niż double, dając możliwość lepszego wykorzystania pamięci podręcznej (cache) i przetwarzania wektorowego.

Stałe matematyczne i podstawowa biblioteka matematyczna

Język C jest językiem "małym" i, jak wiadomo, nawet proste operacje wejścia-wyjścia są w istocie nie częścią języka, ale funkcjami (makrami?) bibliotecznymi. Z drugiej strony jednak, jak zorientowaliśmy się, nie stwarza to programiście żadnych specjalnych niedogodności. Podobnie rzecz ma się z funkcjami matematycznymi. Podstawowe funkcje matematyczne (sin,cos,exp,, itp.) nie są składnikami języka C, lecz w zamian są zaimplementowane w tzw. standardowej bibliotece matematycznej libm.a; prototypy tych funkcji oraz definicje rozmaitych stałych matematycznych: π,e, itp. znajdują się w pliku nagłówkowym math.h. Aby więc skorzystać z tych funkcji w programie, należy

  • w nagłówku pliku, w którym korzystamy z funkcji lub stałych matematycznych, umieścić linię #include <math.h>
  • przy linkowaniu dołączyć bibliotekę matematyczną za pomocą opcji -lm

Przykład

Oto przykładowy prosty program numeryczny w C; drukuje on tablicę wartości sinusów losowo wybranych liczb z przedziału [0,2π].

[{Tablica losowych sinusów}]
  1. include <math.h>
  2. include <stdio.h>
  3. include <stdlib.h> /* zawiera definicję funkcji rand() i stałej RAND_MAX */
  4. define N 15 /* ile liczb wydrukować */

int main(void) { int i; double x,y;

for( i = 0; i < N; i++) { x = rand()/(double)RAND_MAX; x *= 2.0*M_PI; /* oczywiście, wystarczyłoby x =(2.0*M_PI*rand())/RAND_MAX; */ y = sin(x); fprintf(stderr, "(%3d) x = %10.5e sin(x) = %10.5e\n", i, x, y); } return(0); }

Zwróćmy uwagę na linię x = rand()/(double)RAND_MAX; Funkcja rand() zwraca losową liczbę całkowitą z przedziału [0,RAND_MAX], więc iloraz z predefiniowaną stałą RAND_MAX będzie liczbą z przedziału [0,1]. Dla prawidłowego uzyskania losowej liczby z przedziału [0,1] kluczowe jest jednak zrzutowanie jednej z dzielonych liczb na typ double! Gdyby tego nie zrobić, uzyskalibyśmy zawsze x=0 lub sporadycznie x=1, zgodnie z regułą C: "typ wyniku jest zgodny z typem argumentów". Rzeczywiście, w naszym wypadku, (błędna) linia x = rand()/RAND_MAX; zostałaby wykonana tak: ponieważ wynikiem funkcji rand() jest int i stała RAND_MAX jest także typu int, to wynik ma być również typu int -- zostanie więc wykonane dzielenie całkowite; ponieważ mamy rand() RAND_MAX, to wynikiem będzie albo 0, albo 1, i taki rezultat, po zamianie na typ double, zostanie przypisany zmiennej x, co oczywiście nie jest naszym zamiarem. Natomiast, gdy przynajmniej jedna z dzielonych liczb jest typu double, to oczywiście wynikiem też musi być liczba typu double, zostanie więc wykonane zwyczajne dzielenie dwóch liczb zmiennoprzecinkowych (wynik rand() automatycznie zostanie zrzutowany na typ double).

Kompilujemy ten program, zgodnie z uwagami uczynionymi na początku, poleceniem

gcc -o sinusy sinusy.c -lm

Środowisko obliczeń numerycznych: MATLAB i jego klony

Typowa sesja MATLABa. Zwróć uwagę na edytor kodu źródłowego na bieżąco interpretujący go i wychwytujący potencjalne błędy

W końcu lat siedemdziesiątych ubiegłego wieku, Cleve Moler wpadł na pomysł stworzenia prostego interfejsu do ówcześnie istniejących bibliotek numerycznych algebry liniowej: pakietów LINPACK i EISPACK

(których był współautorem). Stworzone przez niego: język skryptów i łatwe w użyciu narzędzia manipulacji macierzami, zdobyły wielką popularność w środowisku naukowym. W 1984 roku Moler skomercjalizował swe dzieło pod nazwą MATLAB (od: "MATrix LABoratory").

MATLAB wkrótce rozrósł się potężnie, implementując (lub wykorzystując) wiele najlepszych z istniejących algorytmów numerycznych, a także oferując bogate możliwości wizualizacji wyników. Dzięki swemu interfejsowi, składającemu się z prostych, niemal intuicyjnych funkcji, oraz ogromnym możliwościom jest jednym z powszechniej używanych pakietów do prowadzenia symulacji komputerowych w naukach przyrodniczych (i nie tylko).

Interpreter MATLABa jest dosyć wolny, dlatego podstawową regułą pisania efektywnych kodów w MATLABie jest maksymalne wykorzystanie gotowych (prekompilowanych) funkcji MATLABa oraz --- zredukowanie do minimum obecności takich struktur programistycznych jak pętle.

Screenshot Octave. W terminalu otwarta sesja w ascetycznym trybie tekstowym, grafika wyświetlana z wykorzystaniem Gnuplota

Kierując się podobnymi przesłankami co C. Moler, oraz bazując na wielkim sukcesie MATLABa, John W. Eaton z Wydziału Inżynierii Chemicznej Uniwersytetu Wisconsin w USA, zaczął w 1994 roku opracowywać darmowe (udostępniane na tzw. licencji GPL) oprogramowanie o funkcjonalności maksymalnie zbliżonej do MATLABa: Octave. Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest intensywnie rozwijana do dziś.

Screenshot Scilaba.

Drugim udanym klonem MATLABa jest francuski Scilab, opracowany w laboratoriach INRIA i wciąż doskonalony. W subiektywnej ocenie autorów niniejszych notatek, nie dorównuje on elegancji i szlachetnej prostocie Octave. Na plus tego pakietu należy zaliczyć m.in. znacznie bardziej rozbudowany zestaw funkcji podstawowych, na minus --- przede wszystkim znacznie mniejszy stopień zgodności z MATLABem, a poza tym niewygodny system pomocy oraz "toporną" (choć o dużym potencjale) grafikę.

Korzystając z dowolnego z omówionych powyżej pakietów, otrzymujemy:

  • możliwość obliczania funkcji matematycznych (nawet dość egzotycznych), finansowych, analizy sygnałów, itp.;
  • bardzo szeroki zakres nowoczesnych narzędzi umożliwiających wykonywanie podstawowych zadań numerycznych, takich jak: rozwiązywanie równań, obliczanie całek, itd.;
  • efektowną wizualizację wyników w postaci wykresów dwu- i trójwymiarowych, a także funkcje do manipulacji obrazem i dźwiękiem;
  • możliwość nieograniczonych rozszerzeń przy użyciu funkcji i skryptów tworzonych przez osoby trzecie (lub własnych).

Jeśli uwzględnić dodatki wielce ułatwiające życie, w tym rozbudowane funkcje wejścia-wyjścia, to narzędzie robi się rzeczywiście intrygujące, jako środowisko obliczeń numerycznych "dla każdego" (nie tylko dla profesjonalnego numeryka). Stąd wielka popularność MATLABa w środowiskach inżynierskich, gdyż umożliwia szybką implementację rozmaitych --- zwłaszcza testowych --- wersji algorytmu, np. przeprowadzania skomplikowanej symulacji. Po zweryfikowaniu wyników można ewentualnie rozpocząć implementację w docelowym środowisku (np. masywnie równoległym komputerze, w Fortranie 95 z wykorzystaniem komercyjnych bibliotek i specyficznymi optymalizacjami kodu), choć bardzo często wyniki uzyskane w MATLABie w zupełności wystarczą.

Zarówno MATLAB, jak Octave i Scilab, opierają się w dużym zakresie na darmowych --- acz profesjonalnych --- zewnętrznych bibliotekach numerycznych (BLAS, LAPACK, FFTW, itp.)

MATLAB jest wciąż niedościgniony jeśli chodzi o narzędzia programistyczne (np. ma wbudowany --- działający na bieżąco w czasie edycji kodu źródłowego! --- debugger, a także kompilator funkcji JIT) oraz możliwości wizualizacji danych i wyników obliczeń. Jeśli zaniedbać koszt zakupu dodatkowych modułów, ma także najbogatszy zestaw funkcji numerycznych. W chwili pisania niniejszego tekstu, (stosunkowo tanie) wersje studenckie MATLABa, nie są oferowane w Polsce przez producenta, choć z drugiej strony są często dostępne (nawet w wersji profesjonalnej) w komputerowych laboratoriach akademickich w Polsce.

Zarówno w MATLABie, jak i w Octave, jest możliwe pisanie funkcji, które będą prekompilowane, dając tym samym znaczące przyspieszenie działania. Najnowsze wersje MATLABa (od wersji 6.5) wykorzystują zresztą domyślnie kompilację JIT --- w trakcie pisania kodu źródłowego funkcji.

Wybór pomiędzy darmowymi klonami MATLABa: Octave i Scilabem, jest właściwie kwestią osobistych preferencji. W ninijeszym kursie będziemy posługiwać się Octave, który jest bardzo wiernym klonem MATLABa, przez co daje zaskakująco wysoki stopień kompatybilności z programami napisanymi w MATLABie. To jest bardzo ważne w praktyce, ponieważ jest dostępnych wiele (nawet darmowych) pakietów numerycznych opracowanych właśnie w języku poleceń MATLABa. Umiejąc posługiwać się Octave, bez trudu można "przesiąść się" na MATLABa i korzystać z jego bogatszych możliwości, droga w przeciwną stronę też jest nietrudna. Inną zaletą Octave jest swobodniejsza niż w MATLABie składnia.

Octave jest dołączany do większości popularnych dystrybucji Linuxa, najczęściej jednak użytkownik musi samodzielnie doinstalować go z płytki instalacyjnej. Ponadto, kody źródłowe najświeższej wersji Octave są dostępne na stronie http://www.octave.org. Dodatkowo, pod http://octave.sourceforge.net znajdziemy pakiet rozszerzeń do Octave, pod nazwą Octave-forge. Wymaga on od użytkowników Linuxa samodzielnej (przebiegającej bezproblemowo) instalacji. Octave można także zainstalować pod Windows, korzystając z programu instalacyjnego dostępnego pod adresem internetowym http://octave.sourceforge.net: w Windowsowej wersji Octave, pakiet Octave-forge jest standardowo dołączony.

Pierwsze kroki w Octave

Octave uruchamiamy poleceniem octave,

[przykry@bit MN] octave GNU Octave, version 2.9.5 (i686-redhat-linux-gnu). Copyright (C) 2006 John W. Eaton. This is free software; see the source code for copying conditions. There is ABSOLUTELY NO WARRANTY; not even for MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, type `warranty'.

Additional information about Octave is available at http://www.octave.org.

Please contribute if you find this software useful. For more information, visit http://www.octave.org/help-wanted.html

Report bugs to <bug@octave.org> (but first, please read http://www.octave.org/bugs.html to learn how to write a helpful report).

octave:1>

Naturalnie, Octave zachowuje wszystkie możliwości kalkulatora naukowego, wykonując podstawowe operacje arytmetyczne i obliczając wartości wielu funkcji matematycznych; oto zapis takiej sesji Octave:

octave:1> 1900/2000 ans = 0.95000 octave:2> sin(pi) ans = 1.2246e-16 octave:3> e^(i*pi) ans = -1.0000e+00 + 1.2246e-16i

Ostatnie dwa wyniki dają nam namacalny dowód, że obliczenia wykonywane są numerycznie, ze skończoną precyzją: w Octave niektóre tożsamości matematyczne są spełnione jedynie w przybliżeniu, np. sin(π)0 oraz eiπ1. Przy okazji widzimy, że Octave dysponuje podstawowymi stałymi matematycznymi (oczywiście, są to także wartości przybliżone!): e 2.71828182845905, pi 3.14159265358979 oraz jednostką urojoną i =1.

Octave ma dobrą dokumentację, dostępną w trakcie sesji Octave; dla każdej funkcji, stałej itp. można uzyskać jej dobry opis przy użyciu polecenia help.

Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz dwuwymiarowa n×m, o elementach rzeczywistych lub zespolonych,

(a11a1man1anm).

W szczególności, biorąc m=1 albo n=1, dostajemy wektor (kolumnowy lub wierszowy

(a1an),lub(a1am).

Z kolei dla m=n=1 mamy do czynienia ze "zwykłymi" liczbami rzeczywistymi lub zespolonymi.

Octave umożliwia, za swoim pierwowzorem --- MATLABem, bardzo wygodną pracę z macierzami.

Wprowadzanie macierzy do Octave jest bardzo intuicyjne i możemy zrobić to na kilka sposobów, w zależności od potrzeb. Można robić to interakcyjnie, podając elementy macierzy z linii komend, na przykład:

octave:6> A = [2 -1 0; -1 3 -2; 2 2.71828 3.14] A =

  2.00000  -1.00000   0.00000
 -1.00000   3.00000  -2.00000
  2.00000   2.71828   3.14000

definiuje macierz kwadratową 3×3 o elementach równych

(21013222.718283.14).

Tak więc, macierz w Octave definiujemy przez podanie jej elementów. Elementy wprowadzamy kolejno wierszami, elementy w wierszu oddzielamy spacjami (lub ewentualnie przecinkami), natomiast kolejne wiersze oddzielamy średnikami.

Macierz możemy także tworzyć stopniowo, wprowadzając jeden za drugim jej kolejne elementy:

octave:3> B(1,1) = 4 B = 4

octave:4> B(2,1) = 3 + B(1,1) B =

 4
 7

octave:5> B(3,2) = 28 B =

  4   0
  7   0
  0  28

Pierwsza komenda określa B jako macierz 1×1, czyli zwykłą liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia -- powoduje, że B zostaje rozszerzony do macierzy 3×2. Zauważmy, że elementom B o nieokreślonych przez nas wartościach zostaje przypisana domyślna wartość zero.

W przypadku, gdy wypełniana macierz jest duża, znacznie korzystniej jest prealokować macierz, ustawiając na samym początku wszystkie elementy macierzy <strong>docelowego wymiaru</strong> (u nas to była macierz <math>\displaystyle 3\times 2</math>) na zero; potem możemy już działać jak poprzednio:

B = zeros(3,2) B(1,1) = 3 + B(1,1) B(2,1) = pi B(3,2) = 28

Jeśli nie chcemy za każdym razem obserwować echa prowadzonych przez nas działań na macierzy B, komendy powinniśmy kończyć średnikiem: średnik po komendzie Octave blokuje echo wyniku jej wykonania. Tak więc, znacznie wygodniej będzie nam napisać

B = zeros(3,2); B(1,1) = 3 + B(1,1); B(2,1) = pi; B(3,2) = 28; B

Ostatnie polecenie -- B bez średnika na końcu -- spowoduje wypisanie zawartości B na ekran.

Tak utworzoną macierz możemy zapisać do pliku, wykorzystując jeden z kilku formatów:

  • tekstowy
  • binarny
  • HDF5

Zapis w formacie tekstowym to po prostu zapis do formatu ASCII. Ze względu na późniejszą konwersję z zapisu w postaci dziesiętnej do dwójkowej, wiąże się z drobnymi niedokładnościami w odczycie.

save macierzb.dat B

W wyniku dostajemy

  1. Created by Octave 2.9.5, Mon Jul 31 17:15:25 2006 CEST <przykry@bit>
  2. name: B
  3. type: matrix
  4. rows: 3
  5. columns: 2
3 0
3.14159265358979 0
0 28

Zapis binarny to prostu zrzut stanu pamięci

save -binary macierzb.dat B

Format HDF5 to format gwarantujący pełną przenośność danych pomiędzy różnymi architekturami, akceptowany przez wiele zewnętrznych aplikacji, m.in. OpenDX.

save -hdf5 macierzb.dat B

Dane z pliku wczytujemy z powrotem poleceniem

load macierzb.dat

Sesję Octave kończymy poleceniem quit.

Notacja dwukropkowa Molera

Do istniejącej macierzy możemy odwoływać się tradycyjnie, czyli do pojedynczych elementów, np. alfa = D(2,2), lub do jej większych bloków, używając popularnej tzw. notacji dwukropkowej Molera.

Wybór bloku w macierzy D przy użyciu notacji dwukropkowej

Jest ona szalenie intuicyjna, a mianowicie: pisząc na przykład v = D(2:5,2) definiujemy (wektor) v, który zawiera wiersze macierzy D od 2 do 5 wybrane z drugiej kolumny. Podobnie, pisząc W = D(2:3,5:7) definiujemy macierz W wymiaru 2×3, o elementach, które zostały wybrane z przecięcia wierszy od 2 do 3 z kolumnami od 5 do 7 macierzy D.

Wybór zestawu kolumn w macierzy D

Dodatkowo, aby odwołać się do całego 4. wiersza (odpowiednio: 5. kolumny) macierzy D, można użyć skrótowej notacji D(4,:) (odpowiednio: D(:,5)).

Odwołanie się do całej macierzy jest także możliwe (przez użycie jej nazwy, np. A = 2*D lub G = log(abs(D))).

Notację dwukropkową można także wykorzystać do wygenerowania samodzielnych zbiorów indeksów:

octave:1> N = 10; octave:2> idx = 1:N idx =

  1   2   3   4   5   6   7   8   9  10

octave:3> idx2 = 1:2:N idx2 =

  1   3   5   7   9

octave:4> nidx = N:-1:1 nidx =

 10   9   8   7   6   5   4   3   2   1

i dlatego pętle, które w C zapisywalibyśmy

for(i=0; i<=N; i++) { ...instrukcje... } for(i=N; i>=1; i--) { ...instrukcje... }

w Octave zapiszemy

for i=0:N ...instrukcje... end for i=N:-1:1 ...instrukcje... end

Za chwilę przekonamy się, jak można w ogóle pozbyć się potrzeby stosowania większości pętli w kodach Octave i MATLABa.

Wektoryzacja w Octave

Ponieważ podstawowymi obiektami w Octave są wektory i macierze, predefiniowane operacje matematyczne wykonują się od razu na całej macierzy. Bez żadnej przesady możena stwierdzić, że umiejętność wektoryzacji i blokowania algorytmów jest podstawą pisania efektywnych implementacji algorytmów w Octave.

Zobaczmy kilka prostych przykładów zawartych w tabeli poniżej. W pierwszej kolumnie tabeli, dane zadanie zaimplementujemy w Octave przy użyciu pętli (zwróćmy przy okazji uwagę na to, jak programuje się pętle w Octave, one nie są zupełnie bezużyteczne...). W drugiej kolumnie zobaczymy, jak to samo zadanie można wykonać korzystając z operatorów lub funkcji macierzowych.

Tradycyjny kod w Octave, używający pętli Efektywny kod wektorowy (macierzowy) w Octave

Alg 1 ||

s = 0; for i = 1:size(x,1) s = s + abs(x(i)); end

s = sum(abs(x));

Alg 2 ||

N = 500; h = (b-a)/(N-1); for i = 1:N x(i) = a + (i-1)*h; y(i) = sin(x(i)); end plot(x,y);

N = 500; x = linspace(a,b,N); y = sin(x); plot(x,y);

Alg 3a ||

for i = 1:size(C,1), for j = 1:size(C,2), for k = 1:size(A,2), C(i,j) = C(i,j) + A(i,k)*B(k,j); end end end

C = C + A*B;

Alg 3b ||

for i = 1:size(C,1), for j = 1:size(C,2), C(i,j) = C(i,j) + A(i,:)*B(:,j); end end

C = C + A*B;

Zwróćmy uwagę na to, że kod wektorowy Octave oraz, w jeszcze większym stopniu, kod macierzowy jest znacznie bardziej elegancki i czytelniejszy od tradycyjnego. Widać to dobrze na wszystkich powyższych przykładach.

Pierwszy przykład pokazuje też, że kod macierzowy jest elastyczniejszy: albowiem obliczy sumę modułów wszystkich elementów x nie tylko gdy x jest wektorem (co musieliśmy milcząco założyć w kodzie w lewej kolumnie), ale tak samo dobrze zadziała, gdy x jest macierzą!

Szczególnie wiele na czytelności i wygodzie zyskujemy w drugim przykładzie, gdzie najpierw funkcja linspace pozwala nam uniknąć żmudnego wyznaczania N równoodległych węzłów xi w odcinku [a,b], a następnie funkcja sin zaaplikowana do całego wektora x daje wartość sinusa w zadanych przez nas węzłach.

Kod wektorowy lub (zwłaszcza) macierzowy jest też znacznie szybszy. Spójrzmy teraz na przykłady: (3a) i (3b), które pokażą nam prawdziwą moc funkcji macierzowych, unikających wielokrotnie zagnieżdżonych pętli. Oba dotyczą operacji mnożenia dwóch macierzy. Przykład (3a) w wersji z potrójną pętlą for naśladuje sposób programowania znany nam z C lub Pascala, natomiast przykład (3b) zdaje się być napisany odrobinę w duchu wektorowym (brak trzeciej, wewnętrznej pętli, zastąpionej operacją wektorową: iloczynem skalarnym i-tego wiersza A i j-tej kolumny B). Poniżej porównanie czasów działania tych trzech implementacji w przypadku macierzy 64×64 (czasy dla PC z procesorem Celeron 1GHz):

  • Dla pętli postaci C(i,j)=C(i,j)+A(i,k)*B(k,j) uzyskano czas 21.6s,
  • Dla pętli postaci C(i,j)=C(i,j)+A(i,:)*B(:,j) --- 0.371s
  • Dla pętli postaci C=C+A*B kod działał jedynie 0.00288s!

Widzimy, jak beznadziejnie wolny jest kod oparty na trzech zagnieżdżonych pętlach: jest on kilka tysięcy razy wolniejszy od implementacji macierzowej C = C + A*B. Po wektoryzacji wewnętrznej pętli, program doznaje kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie wolniejszy od kodu macierzowego!

Literatura

Materiały zgromadzone w kolejnych wykładach są przewodnikiem po zagadnieniach w nich omawianych. Prawdziwe studia wymagają wszelako studiowania --- także dobrej literatury fachowej! Dlatego pod koniec każdego wykładu umieszczamy informację o tym, gdzie szukać pogłębionych informacji na temat omawianego na wykładzie materiału.

Naszym podstawowym źródłem będzie nowoczesny podręcznik

  1. D. Kincaid, W. Cheney, Analiza numeryczna, Wydawnictwa Naukowo-Techniczne, Warszawa, 2006.

Oprócz niego, w języku polskim było wydanych kilka innych podręczników i monografii (niestety, większość z nich dość dawno temu)

  1. M. Dryja, J. i M. Jankowscy, Przegląd metod i algorytmów numerycznych, Wydawnictwa Naukowo-Techniczne, Warszawa, 1988,
  2. A. Bjorck, G. Dahlquist, Metody numeryczne, PWN, Warszawa, XXXX,
  3. Stoer, Bulirsch, Wstęp do analizy numerycznej, PWN, Warszawa, XXXX,
  4. A.Kiełbasiński, H. Schwetlick, Numeryczna algebra liniowa, Wydawnictwa Naukowo-Techniczne, Warszawa, 1992.
  5. A. Ralston, XXXX, PWN, Warszawa, XXXX,
  6. Fortuna, Macukow, , ,

Do tego warto wspomnieć o kilku wartościowych podręcznikach i monografiach w języku angielskim

  1. A. Quarteroni, Valli, Saleri, , ,
  2. P. Deuflhard, A. Hohmann, Numerical Analysis in Modern Scientific Computing. An Introduction, Springer, 2003, ISBN 0-387-95410-4,
  3. K. Atkinson, An Introduction to Numerical Analysis, Wiley, 1989,
  4. , , ,
  5. , , ,
  6. , , ,
  7. , , ,
  8. , , ,
  9. , , ,

Zachęcamy gorąco do zapoznania się z nimi i sztuką obliczeń numerycznych, gdyż jej prawdziwy urok, niuanse i tajemnice --- podobnie jak w przypadku każdej innej sztuki --- może docenić jedynie wyrobiony odbiorca.

Jeśli chodzi o programowanie w Octave lub w MATLABie, warto skorzystać z internetowego podręcznika Octave lub [ co jeszcze].