Programování inženýrských aplikací



Podobné dokumenty
Úvod do OpenMP. Jiří Fürst

Programování v C++ 1, 1. cvičení

Paralení programování pro vícejádrové stroje s použitím OpenMP. B4B36PDV Paralelní a distribuované výpočty

Paralelní architektury se sdílenou pamětí typu NUMA. NUMA architektury

Mělká a hluboká kopie

Více o konstruktorech a destruktorech

Pokročilé programování v jazyce C pro chemiky (C3220) Operátory new a delete, virtuální metody

Vector datový kontejner v C++.

Algoritmizace a programování

Programování v jazyce C pro chemiky (C2160) 3. Příkaz switch, příkaz cyklu for, operátory ++ a --, pole

Funkční objekty v C++.

VÝUKOVÝ MATERIÁL. Bratislavská 2166, Varnsdorf, IČO: tel Číslo projektu

Správné vytvoření a otevření textového souboru pro čtení a zápis představuje

Pointery II. Jan Hnilica Počítačové modelování 17

Vícevláknové programování na CPU: POSIX vlákna a OpenMP I. Šimeček

2 Datové typy v jazyce C

OPS Paralelní systémy, seznam pojmů, klasifikace

Operační systémy. Jednoduché stránkování. Virtuální paměť. Příklad: jednoduché stránkování. Virtuální paměť se stránkování. Memory Management Unit

Základy programování (IZP)

Systém adresace paměti

Martin Flusser. Faculty of Nuclear Sciences and Physical Engineering Czech Technical University in Prague. October 17, 2016

Jazyk C++, některá rozšíření oproti C

Obsah. Předmluva 13 Zpětná vazba od čtenářů 14 Zdrojové kódy ke knize 15 Errata 15

Čtvrtek 8. prosince. Pascal - opakování základů. Struktura programu:

Programování v jazyce C a C++

IUJCE 07/08 Přednáška č. 6

Struktura programu v době běhu

Optimalizace. Optimalizace. Profilování. Gprof. Gcov. Oprofile. Callgrind. Intel Vtune. AMD CodeAnalyst

Základy programování (IZP)

přetížení operátorů (o)

Operační systémy. Cvičení 4: Programování v C pod Unixem

7 Formátovaný výstup, třídy, objekty, pole, chyby v programech

for (i = 0, j = 5; i < 10; i++) { // tělo cyklu }

Operační systémy. Cvičení 3: Programování v C pod Unixem

PROMĚNNÉ, KONSTANTY A DATOVÉ TYPY TEORIE DATUM VYTVOŘENÍ: KLÍČOVÁ AKTIVITA: 02 PROGRAMOVÁNÍ 2. ROČNÍK (PRG2) HODINOVÁ DOTACE: 1

Programování v C++, 2. cvičení

Paralelní a distribuované výpočty (B4B36PDV)

<surface name="pozadi" file="obrazky/pozadi/pozadi.png"/> ****************************************************************************

Přednáška. Správa paměti II. Katedra počítačových systémů FIT, České vysoké učení technické v Praze Jan Trdlička, 2012

Martin Lísal. Úvod do MPI

Koncepce (větších) programů. Základy programování 2 Tomáš Kühr

Střední škola pedagogická, hotelnictví a služeb, Litoměříce, příspěvková organizace

Základy programování (IZP)

ZPRO v "C" Ing. Vít Hanousek. verze 0.3

VÝUKOVÝ MATERIÁL. Bratislavská 2166, Varnsdorf, IČO: tel Číslo projektu

map, multimap - Asociativní pole v C++.

for (int i = 0; i < sizeof(hodnoty) / sizeof(int); i++) { cout<<hodonoty[i]<< endl; } cin.get(); return 0; }

Šablony, kontejnery a iterátory

IAJCE Přednáška č. 8. double tprumer = (t1 + t2 + t3 + t4 + t5 + t6 + t7) / 7; Console.Write("\nPrumerna teplota je {0}", tprumer);

Pokročilé programování v jazyce C pro chemiky (C3220) Statické proměnné a metody, šablony v C++

Programování v C++ 3, 3. cvičení

Paralelní architektury se sdílenou pamětí

Př. další použití pointerů

Programování v C++ 2, 4. cvičení

Úvod do jazyka C. Ing. Jan Fikejz (KST, FEI) Fakulta elektrotechniky a informatiky Katedra softwarových technologií

Pokročilé programování v jazyce C pro chemiky (C3220) Třídy v C++

Konstruktory a destruktory

Vláknové programování část V

Polymorfismus. Časová náročnost lekce: 3 hodiny Datum ukončení a splnění lekce: 30.března

Programování II. Návrh programu I 2018/19

2 Základní funkce a operátory V této kapitole se seznámíme s použitím funkce printf, probereme základní operátory a uvedeme nejdůležitější funkce.

Matematika v programovacích

Základy programování (IZP)

Paralelní výpočetní jádro matematického modelu elektrostatického zvlákňování

Paralelní a distribuované výpočty (B4B36PDV)

1. Programování proti rozhraní

Šablony, kontejnery a iterátory

C++ objektově orientovaná nadstavba programovacího jazyka C

Abstraktní třídy, polymorfní struktury

Implementace numerických metod v jazyce C a Python

1. lekce. do souboru main.c uložíme následující kód a pomocí F9 ho zkompilujeme a spustíme:

Algoritmizace a programování

Mezipaměti počítače. L2 cache. L3 cache

MAXScript výukový kurz

Ústav technické matematiky FS ( Ústav technické matematiky FS ) / 35

Assembler - 5.část. poslední změna této stránky: Zpět

8 Třídy, objekty, metody, předávání argumentů metod

PHP tutoriál (základy PHP snadno a rychle)

1. lekce. do souboru main.c uložíme následující kód a pomocí F9 ho zkompilujeme a spustíme:

Správa paměti. doc. Ing. Miroslav Beneš, Ph.D. katedra informatiky FEI VŠB-TUO A-1007 /

Množina v C++ (set, multiset).

Standardní algoritmy vyhledávací.

Odvozené a strukturované typy dat

Úvod do programování - Java. Cvičení č.4

Procesy a vlákna (Processes and Threads)

Programování v jazyce C a C++

POČÍTAČE A PROGRAMOVÁNÍ

Základy programování. Úloha: Eratosthenovo síto. Autor: Josef Hrabal Číslo: HRA0031 Datum: Předmět: ZAP

Vlákna a přístup ke sdílené paměti. B4B36PDV Paralelní a distribuované výpočty

Výčtový typ strana 67

Příkazy preprocesoru - Před překladem kódu překladačem mu předpřipraví kód preprocesor - Preprocesor vypouští nadbytečné (prázdné) mezery a řádky -

Vyučovací hodina. 1vyučovací hodina: 2vyučovací hodiny: Opakování z minulé hodiny. Procvičení nové látky

PB161 Programování v jazyce C++ Přednáška 4

Množina čísel int stl-set-int.cpp

Pole a kolekce. v C#, Javě a C++

Programování v C++ 2, 8. cvičení

Paralelní architektury se sdílenou pamětí

int ii char [16] double dd název adresa / proměnná N = nevyužito xxx xxx xxx N xxx xxx N xxx N

2) Napište algoritmus pro vložení položky na konec dvousměrného seznamu. 3) Napište algoritmus pro vyhledání položky v binárním stromu.

Transkript:

Výchova studentů pro aplikace řešené na výkonných počítačích České vysoké učení technické v Praze Fakulta strojní Programování inženýrských aplikací Jiří Fürst TENTO PROJEKT JE SPOLUFINANCOVÁN EVROPSKÝM SOCIÁLNÍM FONDEM A STÁTNÍM ROZPOČTEM ČESKÉ REPUBLIKY A ROZPOČTEM HLAVNÍHO MĚSTA PRAHY

Obsah 1 Jemný úvod do C++ 2 1.1 Objektově orientované programování......................... 2 1.2 Knihovna Boost..................................... 4 1.2.1 Vícerozměrná pole v C++ v knihovně Boost................ 4 2 Nástroje pro analýzu kódu 12 2.1 Valgrind......................................... 12 2.1.1 Valgrind - cvičení................................ 12 2.2 Profiler......................................... 12 3 Optimalizace programu pro jeden procesor 14 3.1 Princip fungování cache................................ 14 3.1.1 Přímo mapovaná cache............................ 14 3.1.2 N-cestně asociativní cache........................... 15 3.1.3 Plně asociativní cache............................. 15 3.1.4 Cache na konkrétních typech procesorů................... 15 3.2 Optimalizace pro lepší využití cache......................... 16 3.2.1 Umístění dat v paměti............................. 16 4 Paralelní programování počítače se sdílenou pamětí 19 4.1 Procesy, vlákna a jejich využití............................ 19 4.2 OpenMP standard................................... 20 4.2.1 OpenMP direktivy............................... 20 4.2.2 Direktiva for/do................................ 24 5 Dodatek 27 5.1 Charakteristika počítačů použitých pro měření................... 27 5.2 Program pro měření času............................... 27 Poznámka na úvod Tento text slouží především jako poznámky pro výuku předmětu Programování inženýrských aplikací na FS ČVUT. Jedná se pouze o částečné informace, které si čtenář musí samostatně doplnit a ověřit v literatuře. Některé kapitoly jsou zatím spíše osnovou přednášky a na rozšíření se průběžně pracuje. Soubor pia.pdf obsahuje aktuální verzi tohoto dokumentu v PDF formátu. Na adrese http://marian.fsik.cvut.cz/~furst/pia jsou též průběžně aktualizované verze ukázkových programů a dalších doplňujících materiálů. 1

Kapitola 1 Jemný úvod do C++ V této sekci se pokusíme o ukázku některých prvků programovacího jazyka C++. V žádném případě se nebude jednat o úplný výklad všech zákoutí tohoto jazyka. Pokusíme se spíše čtenáře seznámit s těmi vlastnostmi C++, které jsou dobře využitelné pro tvorbu vědecko-technických programů. Budeme sse snažit hlavně o srozumitelnost a přehlednost programu, dosažení co nejvyššího rychlosti běhu, případnou možnost kontroly přeloženého kódu. Do nedávné doby 1 byl hlavním programovacím jazykem pro vědecko-technické aplikace jazyk Fortran. V současné době se většina studentů učí programovat v jazycích C/C++. Ty jsou univerzálnější než Fortran, na druhou stranu je však programovaní v těchto jazycích poněkud složitější. Proto se budeme zabývat jakousi podmnožinou jazyka C++ pracovně nazvanou C++tran, která bude obsahovat pouze ty konstrukce, které jsou vhodné pro numerické výpočty. Při čtení následujícího textu budeme předpokládat u čtenáře základní znalosti jazyka C. 1.1 Objektově orientované programování Hlavním rozdílem mezi C++ a jazukem C nebo Fortran je velmi dobrá podpora tzv. objektově orientovaného programování. Tomuto způsobu programování je věnována celá řada dobře dostupné literatury. My seproto nebudeme věnovat popisu OOP a ukážeme si pouze některé aplikace OOP, které jsou vhodné pro programovaní jednoduchých úloh z inženýrské praxe. První jednoduchou ukázkou je třída implementující obyčejné jednorozměrné pole. Při práci s poli v jazyce C víme, že je třeba buď znát rozměry pole před sestavením programu (tzv. statické pole), nebo můžeme použít tzv. dynamické pole alokované až v době běhu programu. Bohužel, zpusob zápisu dynamické alokace v C je poněkud složitý a proto se pokusíme nahradit dynamicky alokované pole jednoduchou třídou, kterou budeme dále rozšiřovat. Výpis 1.1 ukazuje velmi jednoduchou třídu, která obsahuje dvě položky: velikost pole a ukazatel na dynamicky alokovaná data. Výhodou této třídy oproti běžné dynamické alokaci je to, že takto alokované pole není třeba explicitně dealokovat. Jakmile totiž zanikne proměnná obsahující pole, volá se destruktor třídy a ten po sobě automaticky uklidí paměť. 1 Dle mého soukromého názoru je jím stále. 2

1 #include <iostream > 2 3 4 c l a s s Array { 5 6 p u b l i c : 7 8 int s i z e ; 9 double data ; 10 11 Array ( int n ) { 12 s i z e = n ; 13 data = new double [ n ] ; 14 } 15 16 ~ Array ( ) { 17 d e l e t e [ ] data ; 18 } 19 20 } ; 21 22 23 // using namespace std ; 24 25 int main ( int argc, char argv ) { 26 27 Array x = Array ( 1 0 ) ; 28 29 for ( int i =0; i <10; i++) 30 x. data [ i ] = i ; 31 32 33 return 0 ; 34 } Výpis 1.1: program class/array.cpp 3

Na druhou stranu, přístup do pole se děje poněkud složitě přes x.data[i]. To není nejvhodnější, protože pokud bychom chtěli nahradit klasické pole pomocí námi navržené třídy v již hotovém programu, museli bychom provádět mnoho změn v kódu a to by mohlo vést k zanesení nových chyb. Bylo by proto vhodné naučit pole pracovat s obvyklým přístupem pomocí hranatých závorek. To je prezentováno v příkladě 1.2, kde v definici třídy přibývá operátor [], který vrací referenci na požadovaný prvek. Další změnou je to, že v této tříde jsou položky deklarované jako privátní a nelze k nim tedy přistupovat přímo. Zápis pomocí x.data[i] tedy již není možný. Důležitější je však to, že uživatel nemá žádnou možnost změnit položku size. Jednou z nejčastějších chyb při práci s poli je překročení rozsahu pole. Například práce s prvkem 100 u pole o velikosti 50. Rozšíříme tedy definici operátoru [] o kontrolu, zda požadovaný prvek je platný. TO lze implementovat jednoduše pomocí rozhodovacího příkazu. To by ale znamenalo, že výsledný kód bude vždy testovat platnost prvku, což může velmi zdržovat běho programu. Ve výpisu 1.3 je proto ukázáno použití makra assert. Přeložíme-li tento program pomocí g++ array-v2a.cpp, bude se při každém přístupu k prvku pole testovat podmínka uvedená uvnitř makra. Jestliže nebude splněna, progrm ohlásí při behu chybu s výpisem, pomocí kterého budeme schopni chybu lokalizovat. Pro výslednou verzi programu, která již neobsahuje chyby, můžeme použít překlad s parameterm -DNDEBUG, překladač automaticky vynechá všechny kontroly v makru assert. Dalším omezení polí v C/C++ je to, že jsou indexována výhradně od nuly. Výpis 1.4 ukazuje rozšíření třídy Array o možnost deklarovat pole s libovolnými mezemi. Poslední ukázkou implementace pole je příklad 1.5, ketrý ukazuje, jak jednoduše použít navrženou třídu Array nejen pro prvky typu float, ale pro definici pole libovolného typu. 1.2 Knihovna Boost Knihovna Boost je volně dostupná na adrese http://www.boost.org. Jedná se o velmi rozsáhlou knihovnu usnadňující programování některých obecných úloh v jazyce C++. Její úplný popis se vymyká rozsahu tohoto kurzu a my se proto omezíme na podmnožinu, týkající se implementace polí. 1.2.1 Vícerozměrná pole v C++ v knihovně Boost Podpora vícerozměrných polí je v jazyce C++ velmi omezená. Je sice možné deklarovat vícerozměrné pole například jako double a[100][100], tato deklarace je však statická. Pokud je zapotřebí použít dynamická pole, řeší se to obyčejně tak, že se na vícerozměrné pole pohlíží jako na pole polí, např. pro dvourozměrná pole (matici) bychom použili pole ukazatelů na rádky matice. Tento postup vypadá na první pohled velmi atraktivně: je možné použít jednoduchý zápis pro přístup k prvku jako a[i][j] a je možné postup rozšířit i na vícerozměrná pole. Na druhou stranu má tento postup určité nevýhody: složitější alokace a dealokace pole, např. pro matici se alokace provádí jako double** a;... a = (double**)malloc(n*sizeof(double*)); for (i=0; i<n; i++) a[i] = (double*)malloc(m*sizeof(double)); a dealokace jako 4

1 #include <iostream > 2 #include <c a s s e r t > 3 4 c l a s s Array { 5 6 int s i z e ; 7 double data ; 8 9 p u b l i c : 10 11 Array ( int n ) { 12 s i z e = n ; 13 data = new double [ n ] ; 14 } 15 16 double & o p e r a t o r [ ] ( int i ) { 17 return data [ i ] ; 18 } 19 20 21 ~ Array ( ) { 22 d e l e t e [ ] data ; 23 } 24 25 int g e t S i z e ( ) { 26 return s i z e ; 27 } 28 29 } ; 30 31 32 using namespace std ; 33 34 int main ( int argc, char argv ) { 35 36 Array x = Array ( 1 0 ) ; 37 38 for ( int i =0; i <x. g e t S i z e ( ) ; i ++) 39 x [ i ] = i ; 40 41 double s =0; 42 for ( int i =0; i <x. g e t S i z e ( ) ; i ++) 43 s += x [ i ] ; 44 45 cout << " s = " << s << endl ; 46 47 return 0 ; 48 } Výpis 1.2: program class/array-v2.cpp 5

1 #include <iostream > 2 #include <c a s s e r t > 3 4 c l a s s Array { 5 6 int s i z e ; 7 double data ; 8 9 p u b l i c : 10 11 Array ( int n ) { 12 s i z e = n ; 13 data = new double [ n ] ; 14 } 15 16 double & o p e r a t o r [ ] ( int i ) { 17 return data [ i ] ; 18 } 19 20 21 ~ Array ( ) { 22 d e l e t e [ ] data ; 23 } 24 25 int g e t S i z e ( ) { 26 return s i z e ; 27 } 28 29 } ; 30 31 32 using namespace std ; 33 34 int main ( int argc, char argv ) { 35 36 Array x = Array ( 1 0 ) ; 37 38 for ( int i =0; i <x. g e t S i z e ( ) ; i ++) 39 x [ i ] = i ; 40 41 double s =0; 42 for ( int i =0; i <x. g e t S i z e ( ) ; i ++) 43 s += x [ i ] ; 44 45 cout << " s = " << s << endl ; 46 47 return 0 ; 48 } Výpis 1.3: program class/array-v2-a.cpp 6

Výpis 1.4: program class/array-v3.cpp 1 #include <iostream > 2 #include <c a s s e r t > 3 4 c l a s s Range { 5 6 p u b l i c : 7 8 int lower, upper ; 9 10 Range ( ) { } ; 11 12 Range ( int up ) { 13 lower = 0 ; 14 upper = up ; 15 } 16 17 Range ( int lo, int up ) { 18 lower = l o ; 19 upper = up ; 20 } 21 22 } ; 23 24 25 c l a s s Array { 26 27 Range range ; 28 double data ; 29 30 p u b l i c : 31 32 Array ( Range r ) { 33 range = r ; 34 data = new double [ range. upper range. lower ] ; 35 } 36 37 Array ( int n ) { 38 range = Range ( n ) ; 39 data = new double [ range. upper range. lower ] ; 40 } 41 42 43 double& o p e r a t o r [ ] ( int i ) { 44 a s s e r t ( range. lower<= i & i < range. upper ) ; 45 return data [ i range. lower ] ; 46 } 47 48 int lbound ( ) { return range. lower ; } 49 50 int ubound ( ) { return range. upper ; } 51 52 ~ Array ( ) { 53 d e l e t e [ ] data ; 54 } 55 56 } ; 57 58 59 using namespace std ; 60 61 int main ( int argc, char argv ) { 62 63 Array x = Array ( 1 0 ) ; 64 65 for ( int i =0; i <10; i++) 66 x [ i ] = i ; 67 68 double s ; 69 s = 0 ; 70 for ( int i =0; i <10; i++) 71 s += x [ i ] ; 72 73 cout << " s = " << s << endl ; 74 75 7 76 77 Array y = Array ( Range ( 2,12) ) ; 78 79 for ( int i=y. lbound ( ) ; i <y. ubound ( ) ; i ++) 80 y [ i ] = i ; 81 82 s = 0 ; 83 for ( int i=y. lbound ( ) ; i <y. ubound ( ) ; i ++) 84 s += y [ i ] ; 85 86 cout << " s = " << s << endl ; 87 88 89 return 0 ; 90 }

1 #include <iostream > 2 #include <c a s s e r t > 3 4 template<typename T> 5 c l a s s Array { 6 7 int s i z e ; 8 T data ; 9 10 p u b l i c : 11 12 Array ( int n ) { 13 s i z e = n ; 14 data = new T[ n ] ; 15 } 16 17 T & o p e r a t o r [ ] ( int i ) { 18 a s s e r t ( 0<= i & i < s i z e ) ; 19 return data [ i ] ; 20 } 21 22 23 ~ Array ( ) { 24 d e l e t e [ ] data ; 25 } 26 27 } ; 28 29 30 using namespace std ; 31 32 int main ( int argc, char argv ) { 33 34 //Array x = Array ( 1 0 ) ; 35 Array<float > x ( 1 0 ) ; 36 37 for ( int i =0; i <10; i++) 38 x [ i ] = i ; 39 40 double s =0; 41 for ( int i =0; i <10; i++) 42 s += x [ i ] ; 43 44 cout << " s = " << s << endl ; 45 46 return 0 ; 47 } Výpis 1.5: program class/array-v4.cpp 8

Výpis 1.6: program boost/array2d.cpp 1 #include <iostream > 2 #include " boost / multi_array. hpp " 3 4 typedef boost : : multi_array<double,2> array2d ; 5 6 int main ( int argc, char argv ) { 7 int n, m; 8 double s ; 9 10 s t d : : cout << " Zadej rozmery p o l e ( n,m)\ n " ; 11 s t d : : c i n >> n >> m; 12 13 array2d A( boost : : e x t e n t s [ n ] [m] ) ; 14 15 for ( int i =0; i<n ; ++i ) 16 for ( int j =0; j<m; ++j ) 17 A[ i ] [ j ] = 1. 0 ; 18 19 s = 0 ; 20 for ( int i =0; i<n ; ++i ) 21 for ( int j =0; j<m; ++j ) 22 s += A[ i ] [ j ] ; 23 24 s t d : : cout << " Soucet n x m j e " << s << " \n " ; 25 return 0 ; 26 } for (i=0; i<n; i++) free(a[i]); free(a); pole jsou indexována vždy od nuly, není možné jednoduchým způsobem z pole vybrat určitou část (podmatici). Tyto problémy řeší šablona multi_array<t,rank>, která definuje pole o rank rozměrech jehož prvky jsou typu T. Následující příklad ukazuje deklaraci matice o rozměrech n krát m prvků typu double: #include "boost/multi_array.hpp"... boost::multi_array<double,2> a(boost::extents[n][m]); K prvkům této matice lze přistupovat 2 jednoduše pomocí indexů, tj. a[i][j]. Příklad 1.6 ukazuje jednoduché použití knihovny boost pro práci s maticemi. Na zčátku kódu je pomocí příkazu typedef definován nový datový typ array2d, který odpovídá matici s prvky typu double. Pole definované pomocí šablony multi_array obsahuje mimo hodnot prvků také informace o tvaru pole (mj. o počtu dimenzí, o velikostech v jednotlivých dimenzích). Toho lze využít například při předávání pole do funkce. Příklad 1.7 ukazuje použití metody shape(), která pro zadané pole vrací odkaz na pole obsahující rozměry pole v jednotlivých dimenzích. Velikou výhodou je možnost definovat pole, jejichž indexu nepůjdou od nuly, ale od libovolného čísla. Příklad 1.8 ukazuje, jak lze vytvořit pole, jehož indexy začínají -1. Všimněte si též funkce pro součet prvků. Zde je nejprve z pole získána hosdnota nejmenšího indexu pomocí index_bases(). 2 Existují i další typy přístupů k prvkům matice, například přes iterátory. 9

Výpis 1.7: program boost/array2d-s1.cpp 1 #include <iostream > 2 #include " boost / multi_array. hpp " 3 #include <c a s s e r t > 4 5 6 typedef boost : : multi_array<double,2> array2d ; 7 8 double sum( array2d M) { 9 double s ; 10 11 s = 0 ; 12 for ( int i =0; i <M. shape ( ) [ 0 ] ; ++i ) 13 for ( int j =0; j<m. shape ( ) [ 1 ] ; ++j ) 14 s += M[ i ] [ j ] ; 15 16 return s ; 17 } ; 18 19 int main ( int argc, char argv ) { 20 int n, m; 21 double s ; 22 23 s t d : : cout << " Program pro demonstraci 2D p o l e z knihovny b o ost \n " ; 24 s t d : : cout << " Zadej rozmery p o l e ( n,m)\ n " ; 25 s t d : : c i n >> n >> m; 26 s t d : : cout << " zadano n=" << n << " m=" << m << " \n " ; 27 28 array2d A( boost : : e x t e n t s [ n ] [m] ) ; 29 30 for ( int i =0; i<n ; ++i ) 31 for ( int j =0; j<m; ++j ) 32 A[ i ] [ j ] = 1. 0 ; 33 34 s = sum (A) ; 35 36 s t d : : cout << " Soucet n x m j e " << s << " \n " ; 37 return 0 ; 38 } 10

Výpis 1.8: program boost/array2d-m1.cpp 1 #include <iostream > 2 #include " boost / multi_array. hpp " 3 #include <c a s s e r t > 4 5 6 typedef boost : : multi_array<double,2> array2d ; 7 typedef array2d : : extent_range range ; 8 9 double sum( array2d M) { 10 double s ; 11 12 int i 1 = M. index_bases ( ) [ 0 ] ; 13 int i 2 = i 1 + M. shape ( ) [ 0 ] ; 14 15 int j 1 = M. index_bases ( ) [ 1 ] ; 16 int j 2 = j 1 + M. shape ( ) [ 1 ] ; 17 18 for ( int i=i 1 ; i <i 2 ; ++i ) 19 for ( int j=j 1 ; j<j 2 ; ++j ) 20 s += M[ i ] [ j ] ; 21 22 return s ; 23 } ; 24 25 int main ( int argc, char argv ) { 26 int n, m; 27 double s ; 28 29 s t d : : cout << " Program pro demonstraci 2D p o l e z knihovny b o ost \n " ; 30 s t d : : cout << " Zadej rozmery p o l e ( n,m)\ n " ; 31 s t d : : c i n >> n >> m; 32 s t d : : cout << " zadano n=" << n << " m=" << m << " \n " ; 33 34 array2d A( boost : : e x t e n t s [ range ( 1,n + 1 ) ] [ range ( 1,m+ 1 ) ] ) ; 35 36 for ( int i = 1; i<n+1; ++i ) 37 for ( int j = 1; j<m+1; ++j ) 38 A[ i ] [ j ] = 1. 0 ; 39 40 s = sum (A) ; 41 42 s t d : : cout << " Soucet ( n+2) x (m+2) j e " << s << " \n " ; 43 return 0 ; 44 } 11

Kapitola 2 Nástroje pro analýzu kódu 2.1 Valgrind Program valgrind umožňuje nalézt v přeloženém programu místa, která jsou pravděpodobně chybami. Obsahuje následující nástroje: memcheck - nástroj pro odhalování chybných přístupů do paměti (např. index prvku v poli je mimo rozsah pole), použití neinicializovaných proměnných a chybné alokace a dealokace, cachegrind - nástroj pro měření využití cache, callgrind - nástroj pro měření výkonu programu, massif - nástroj pro měření paměťových nároků programu. 2.1.1 Valgrind - cvičení Pomocí programu valgrind se pokuste nalézt a odstranit chyby z následujících programů. valgrind/example_01.cpp valgrind/example_02.cpp valgrind/example_03.cpp valgrind/example_04.cpp valgrind/example_05.cpp valgrind/example_06.cpp valgrind/example_07.cpp 2.2 Profiler Profiler je program umožňující změřit výkon programu. Velmi jednoduché je použití standardního profileru gprof. Na následujícím příklade si jej ukážeme na příkladě kódu pro řešení obtékání profilu. Jedná se program obsahující celou řadu funkcí a přetížených operátorů. My se pokusíme 12

zjistit, kolikrát se která funkce ve skutečnosti volala a kolik času se v ní strávilo. To nám pomůže určit, kterou část kódu bychom měli optimalizovat. Nejprve program přeložíme s parametrem -pg, tedy g++ -O -pg ProgramLW-orig.cpp Poté jej spustime. Během vykonávání programu se v aktuálním adresáři vytváří soubor gmon.out, do kterého si program zapisuje informace o svém běhu. Tyto informace lze přečíst pomocí programu gprof, tedy gprof./a.out Na obrazovce se objeví dlouhý výpis začínající udaji o jednotlivých funkcích. V našem případě je zde něco jako Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 33.61 0.64 0.64 28195593 0.00 0.00 tpromenna::operator*(double) 33.34 1.27 0.63 1 0.63 1.87 NumerickeSchema() 25.93 1.76 0.49 19702179 0.00 0.00 tpromenna::operator+(tpromenna) 6.35 1.88 0.12 8520200 0.00 0.00 tpromenna::operator-(tpromenna) 0.79 1.89 0.02 frame_dummy 0.00 1.89 0.00 1 0.00 0.00 global constructors keyed to _ZN9tPromennap 0.00 1.89 0.00 1 0.00 0.00 NodeCentering() 0.00 1.89 0.00 1 0.00 0.00 SpoctiGeometrii() 0.00 1.89 0.00 1 0.00 0.00 VypisVysledekNC() 0.00 1.89 0.00 1 0.00 0.00 static_initialization_and_destruction_0(i 0.00 1.89 0.00 1 0.00 0.00 Zadani() 0.00 1.89 0.00 1 0.00 0.00 PocPodm() 0.00 1.89 0.00 1 0.00 0.00 NactiSit() Z tohoto výpisu vidíme, že prakticky třetina času se strávila během volání operátoru násobení a tento operátor byl volán mnohokrát. Stejně tak se mnoho časy strávilo ve funkci NumerickeSchems() a v operátoru pro sčítání. Naproti tomu funkce jako SpoctiGeomerii() nespotřebuje prakticky žádný čas. To znamená, že při optimalizaci kůdu bychom se měli soustředit hlavně na operátory a na funkci NumerickeSchema(). 13

Kapitola 3 Optimalizace programu pro jeden procesor Současné procesory jsou většinou velice výkonné a při práci s rozsáhlými daty, se kterými se u vědecko-technických aplikací často setkáváme, nebývá celkový výkon počítače určován ani tak frekvencí procesoru, jako spíše rychlostí přístupu do paměti. Výroba pamětí, které by pracovaly s rychlostí odpovídající procesoru, je velmi drahá a náročná a proto se v počítačích využívají pomalejší paměti s velkou kapacitou a procesory se vybavují rychlejšími vyrovnávacími pamětmi (tzv. cache) o menší kapacitě. 3.1 Princip fungování cache Jak již bylo naznačeno, cache bývá sice mnohem rychlejší, má však mnohem menší kapacitu, než hlavní pamět. To znamená, že se do cache nemohou vejít všechna potřebná data a řídicí obvody cache musí umět "hospodařit" s místem. To vše musí zvládat velmi rychle a proto algoritmus pro správu cache musí být velmi jednoduchý. Probereme si nyní tři v současné době nejužívanější typy cache pamětí. 3.1.1 Přímo mapovaná cache Nejjednodušším typem je tzv.přímo mapovaná cache. Její princip si nejlépe vysvětlíme na jednoduchém příkladě. Předpokládejme, že náš fiktivní 32-bitový procesor má 16kB cache. Ta bývá organizovaná to tzv. řádků. Řekněme, že délka řádku je 64B, to znamená, že cache má 256 řádků. Ke každému řádku je navíc přidruženo dodatečných 18 bitů paměti (tzv. tag) pro řízení cache. Předpokládejme, že procesor chce číst dataz určité adresy, provede řadič následující operace: 1. rozdělí adresu na tři části. V pořadí od nejméně významného bitu to jsou (a) 6 bitů tvořících adresu bytu v řádku o délce 64B, (b) 8 bitů tvořících číslo řádku, (c) zbylých nejvýznamnějších 18 bitů pro tag. 2. Řadič se podívá do vypočteného řádku a pokud uložený tag v tomto řádku souhlasí s tagem adresy požadovaných dat, přečte procesor data z odpovídající pozice tohoto řádku. 14

3. Pokud tag nesouhlasí, řádek z cache je uložen do paměti (pokud je to zapotřebí) a na jeho místo je z paměti natažen celý řádkek obsahující požadovaná data. To znamená, že komunikace mezi procesorem a pamětí probíhá po celých řádcích (tj. 64B) a to i v případě, že potřebujeme z paměti jediný bit. Tento nejjednodušší typ cache trpí jednou nevýhodou. Představme si, že pracujeme s matsicí čísel typu double o rozměrech 2048x2048 a tato čísla budou uložena v souvislém úseku paměti. Předpokládejme, že chceme sečíst v této matici první dva sloupce 1 a výsledek uložit do třetího: 1 for ( i =0; i <2048; i ++) 2 a [ i ] [ 2 ] = a [ i ] [ 0 ] + a [ i ] [ 1 ] ; Počítač nejdříve načte do cache číslo a[0][0]. Pak se snaží načíst a[0][1]. To je přesně 16kB od a[0][0] a mělo by tedy být uloženo v cache do stejného řádku. Je tedy třeba načíst nový řádek (tj. 64B). Výsledek se bude ukládat do a[0][2], které patří opět do stejného řádku a je tedy potřeba opět načíst do stejného řádku cache data z jiného úseku paměti. Ve druhém kroku cyklu se situace přesně opakuje s tím rozdílem, že než se do cache nahraje řádek obsahující a[1][0], je třeba stávající řádek zapsat do paměti (bylo v něm změněno 8B). Ve výsledku to znamená, že jsme nejen vůbec nevyužili rychlost cache, ale k načtení 2 n a uložení n čísel o 8B se z pomalé paměti načítalo 3 n a ukládalo n řádků o délce 64B! 3.1.2 N-cestně asociativní cache V mnoha moderních procesorech se proto používá tzv. vícecestná nebo vícecestně asociativní cache. Tu si můžeme představit zhruba jako několik vedle sebe postavených přímo mapovaných pamětí. Např. L1D cache procesoru T2350 (Intel Core Duo) má kapacitu 32kB a je 8-mi cestně asociativní. To znamená, že je složena z osmi bank po 64 řádcích o délce 64B. Ke každému z těchto 512 řádků je přidána paměť o délce 20b pro tagy. V případě, že chce procesor procesor číst nějaká data, rozdělí adresu na dolních 6 bitů pro určení pozice uvnitř řádku, dalších 6 bitů pro číslo řádku a zbylých 20 bitů bere jako tag. Řadič potom porovná vypočtený tag s uloženými tagy v daném řádku pro každou banku a pokud v některé dojde ke shodě, přečte data z řádku této banky. Pokud není požadovaný řádek v žadné bance, řadič uvolní řádek v některé bance a nahraje požadovaný řádek do tohoto místa. Tím se do jisté míry odstraní resp. oddálí dříve zmiňovaný problém. Nicméně stejný problém by mohl nastat v případě, že bychom chtěli sečíst prvních 9 sloupců matice o rozměrech 512 512 prvků. 3.1.3 Plně asociativní cache Nejdokonalejším, avšak výrobně nejnáročnějším) typem je tzv. plně asociativní cache, kterou si můžeme představit jako N-cestně asociativní typ, kde N je tak velké, že v každé bance je pouze jeden řádek. Např. pro 32kB cache s řádkem dlouhým 64B by to odpovídalo 512-cestně asociativní verzi. 3.1.4 Cache na konkrétních typech procesorů Toto bych rád doplnil tabulkou shrnující parametry cache na některých procesorech. 1 Ve Fortranu by to byly řádky. 15

3.2 Optimalizace pro lepší využití cache 3.2.1 Umístění dat v paměti Rychlost přístupu dat do paměti je mimo jiné ovlivněna rozložením dat v paměti. Uvažujme následující příklad: máme pole struktur z nichž každá obsahuje tři čisla typu double. Naším úkolem je vypočíst nové pole o stejné velikosti jehož hodnoty budou dány součtem deseti prvků původního pole na zadaných pozicích. Tento algoritmus se nazývá gathering a v různých variantách se objevuje např. při násobení řídkých matic s vektory nebo v metodě konečných prvků či objemů. Podívejme se nyní na to, jakým způsobem je možné tento jednoduchý algoritmus optimalizovat pro lepší využití cache. Připomeňme, že při přístupu k paměti se do cache přenáší celá řádka (tzn. 64B). To znamená, že pokud je načítaná struktura uložená v paměti uvnitř jednoho řádku, bude pro načtení jedné struktury o délce 24B zapotřebí načíst z paměti 64B. Pokud ale bude uložena přes hranici řádků, bude potřeba načíst 128B! To znamená, že je vhodné ukládat data tak, aby neprocházela přez hranici řádků. Tho lze dosáhnout jednoduše ve dvou krocích: Modifikujeme data tak, aby délka struktury odpovídala délce řádku, tzn. aby byla buď dělitelem nebo násobkem délky řádku. V našem případě do struktury doplníme nepoužívanou položku char pad[8] čímž změníme délku struktury na 32B. Alokujeme pamět tak, aby začátek alokované paměti odpovídal hranici řádků. Toho dosáhneme např. pomocí funkce posix_memalign. Výpis 3.1 obsahuje implementaci tohoto programu. Tabulka 3.1 ukazuje vliv délky datové struktury a adresy začátku pole na rychlost programu pro různé typy procesorů. je z ní vidět, že uložení dat v paměti může ovlivnit výkon programu. Na druhou stranu je ale u tohoto příkladu vidět závislost na daném procesoru. 16

Výpis 3.1: program cache/padding.cpp 1 #include <iostream > 2 #include <s t d l i b. h> 3 #include " timer. hpp " 4 5 struct Item { 6 double x ; 7 double y ; 8 double z ; 9 10 char pad [32 3 s i z e o f ( double ) ] ; 11 } ; 12 13 14 15 int main ( int argc, char argv ) { 16 const int N = 1 0 0 0 0 0 0 ; 17 const int NZ = 1 0 ; 18 const int NITER = 5 ; 19 20 Item a, b ; 21 int idx, k ; 22 double x, y, z ; 23 24 //a = ( Item ) malloc (N s i z e o f ( Item ) ) ; 25 //b = ( Item ) malloc (N s i z e o f ( Item ) ) ; 26 27 posix_memalign ( ( void )&a, 64, N s i z e o f ( Item ) ) ; 28 posix_memalign ( ( void )&b, 64, N s i z e o f ( Item ) ) ; 29 30 idx = ( int ) malloc (NZ N s i z e o f ( int ) ) ; 31 32 for ( int i =0; i <N NZ; i ++) 33 idx [ i ] = ( int ) (N ( ( f l o a t ) rand ( ) ) /RAND_MAX) ; 34 35 for ( int i =0; i <N; i ++) { 36 b [ i ]. x = 1 ; b [ i ]. y = 2 ; b [ i ]. z = 3 ; 37 } ; 38 39 40 Timer stopky ( " mereni " ) ; 41 stopky. s t a r t ( ) ; 42 43 for ( int i t e r =0; i t e r <NITER ; i t e r ++) 44 for ( int i =0; i <N; i ++) { 45 x = 0 ; y = 0 ; z = 0 ; 46 for ( int j=nz i ; j<nz i+nz; j++) { 47 k = idx [ j ] ; 48 x += b [ k ]. x ; 49 y += b [ k ]. y ; 50 z += b [ k ]. z ; 51 } ; 52 a [ i ]. x = x ; 53 a [ i ]. y = y ; 54 a [ i ]. z = z ; 55 } ; 56 57 stopky. s t o p ( ) ; 58 59 std : : cout << "MFLOPS = " << (3 N NZ NITER)/ stopky. getwalltime ( ) 1. e 6; 60 s t d : : cout << " \n " ; 61 s t d : : cout << " s i z e o f ( Item)= " << s i z e o f ( Item ) << " \n " ; 62 s t d : : cout << "&a [ 0 ] = " << a << " \n " ; 63 s t d : : cout << "&b [ 0 ] = " << b << " \n " ; 64 return 0 ; 65 } 17

CPU sizeof(item) alokace MFLOPS Pentium-M 24 malloc 35-1.5GHz 32 malloc 37 - g++ -O3 24 posix_memalign 35 32 posix_memalign 37 Pentium-M 24 malloc 38-1.5GHz 32 malloc 46 - icc -O3 -xb 24 posix_memalign 38 32 posix_memalign 47 Core 2 Duo 24 malloc 90 - E6600 32 malloc 80-2.4GHz 24 posix_memalign 91 - g++ -O3 32 posix_memalign 108 Altix 24 malloc 43 - Itanium2 32 malloc 43-1.5GHz 24 posix_memalign 43 - icc -O3 32 posix_memalign 43 Altix 24 malloc 18 - Itanium2 32 malloc 17-1.5GHz 24 posix_memalign 18 - g++ -O3 32 posix_memalign 18 Tabulka 3.1: Výkon programu cache/padding.cpp na ruzných typech procesorů. CPU LDA MFLOPS Pentium-M 1024 20 - g++ -O3 1025 50 Pentium-M 1024 248 - icc -O3 -xb 1025 249 Core 2 Duo 1024 - g++ -O3 1025 Altix 1024 20 - icc -O3 1025 90 Altix 1024 345 - icc91 -O3 1025 342 Tabulka 3.2: Výkon programu cache/trashing.cpp na ruzných typech procesorů. 18

Kapitola 4 Paralelní programování počítače se sdílenou pamětí Nejprve se soustředíme na programování počítačů se sdílenou pamětí. U tohoto typu paralelního počítače mají všechny procesory neomezený přístup do společné paměti. Velmi často je takový počítač složen z jedné společné paměti ke které je přes společnou sběrnici připojeno buď několik procesorů nebo jeden vícejádrový procesor. S rostoucím počtem procesorů resp. jader zde však dochází velmi rychle dochází k zahlcení sběrnice a proto je tento jednoduchý model vhodný pouze pro malý počet procesorů. Složitějším typem počítače se sdílenou pamětí je tzv. NUMA systém (Non-Uniform Memory Access neboli nestejný přístup k paměti), kdy je počítač složen z uzlů obsahujících jeden či více procesorů a lokální paměť. Uzly jsou navzájem propojeny pomocí řídicích obvodů a sítě tak, že procesory mají neomezený přístup do pamětí všech uzlů (sdílená paměť), rychlost přístupu však závisí na tom, do jaké části paměti procesor přistupuje. Nejrychlejší přístup je samozřejmě do paměti na stejném uzlu, rychlost přístupu do paměti jiných uzlů závisí na topologii propojení a na rychlosti linek, bývá však několikanásobně menší. Tento typ počítače (např. SGI Altix) existuje v konfiguracích obsahujících tisíce procesorů. V následujících kapitolách se seznámíme s přímým využitím procesů a vláken pro nízkoúrovňové paralelní programování a dále se budeme podrobněji věnovat standardu OpenMP, který umožňuje paralelní programování na vyšší úrovni. 4.1 Procesy, vlákna a jejich využití Hlavním cílem paralelního programování je zkrácení času potřebného na zpracování nějaké úlohy. Toho lze dosáhnout tak, že ke zpracování této úlohy využijeme více procesorů. Můžeme tedy spustit několik procesů nebo jeden proces rozdělit na několik vláken tzv. threads. Objasněme si jeden podstatný rozdíl mezi procesem a vláknem. Každý proces má svou vlastní přidělenou paměť která je chráněna před přístupem od ostatních procesů. To znamená, že procesy mezi sebou nemohou komunikovat přímo a musí k tomu využívat služeb operačního systému. Navíc se zde do jisté míry ztrácí výhoda počítače se sdílenou pamětí. Naprotitomu vlákna vzniknou rozštěpením jednoho procesu a sdílejí tedy spolu paměť. Komunikovat spolu tedy mohou přímo pomocí této sdílené paměti. Odsud vyplývá, že pro paralelní programování na počítačích se sdílenou pamětí je výhodnější používat vlákna. Přímé použití vláken lze například pomocí knihovny pthreads, která obsahuje základní funkce 19

1 #include <s t d i o. h> 2 3 int main ( ) { 4 5 #pragma omp p a r a l l e l 6 { 7 p r i n t f ( " Ahoj\n " ) ; 8 } 9 10 return 0 ; 11 } Výpis 4.1: program openmp/parallel.c pro vytváření a rušení vláken a pro synchronizaci mezi vlákny. 4.2 OpenMP standard OpenMP je nadstavbou jazyků Fortran/C/C++, která umožňuje paralelizaci programu bez nutnosti práce s nizkoúrovňovými knihovnami typu pthreads. Standard OpenMP (viz www.openmp.org) je rozdělen na tři části: 1. OpenMP direktivy - v podstatě se jedná o pokyny pro překladač, jak má paralelizovat určité části programu, 2. funkce OpenMP knihovny - zde je soustředěno několik funkcí, které umožňují např. zjistit počet běžících vláken, číslo aktuálního vlákna atd. 3. systémové proměnné - pomocí nich lze nastavit různé parametry pro běh programu. K tomu, aby bylo možné využít OpenMP standard, je nutné používat překladač, ktreý OpenMP podporuje a pomocí vhodného přepínače tuto podporu zapnout. V současné době se jedná mimo jiné o překladače firmy Intel nebo překladače GNU verze 4.2 a výš. 4.2.1 OpenMP direktivy OpenMP direktivy se skládají z úvodní části (v jazyce C/C++ je to #pragma omp, ve Fortranu!$omp nebo C$omp), která říká překladači, že bude následovat tělo OpenMP direktivy, a z vlastního těla direktivy. Direktiva parallel Direktiva parallel způsobí rozdělení programu na několik vláken. Tato vlákna provádějí v C/C++ následující příkaz (nebo posloupnost příkazů uzavřených do složených závorek), v jazyce Fortran provádějí následující příkazy až k direktivě end parallel. Jednoduchý příklad této direktivy je uveden ve výpisu 4.1 pro C/C++ a 4.2 pro Fortran. Program přeložíme např. pomocí překladače Intel následovně icc -O -openmp parallel.c a na obrazovce uvidíme výpis parallel.c(5): (col. 1) remark: OpenMP DEFINED REGION WAS PARALLELIZED. 20

1 program P a r a l l e l 2 3!$omp p a r a l l e l 4 print, " Ahoj " 5!$omp end p a r a l l e l 6 7 end program P a r a l l e l Výpis 4.2: program openmp/parallel.f90 Ten nás informuje o tom, že příkaz začínající na řádku 5 byl paralelizován. Podobně se překládá i program ve Fortranu a to příkazem resp. ifort -O -openmp parallel.f90 Pro případ GNU překladačů od verze 4.2 je překlad pomocí gcc -O -fopenmp parallel.c gfortran -O -fopenmp parallel.f90 Podívejme se nyní na to, jakým způsobem se vykonává tento program. Po spuštění příkazem./a.out se rozběhne jedno hlavní vlákno zpracovávající náš program. Jakmile toto vlákno dorazí na direktivu parallel, dojde k jeho rozdělení na několik vláken, které vykonávají příkaz pro tisk na obrazovku. Všechna vlákna však vykonávají totéž. To znamená, že na obrazovce bychom měli uvidět tolik kopií 1 výpisu, kolik se spustilo vláken. Jakmile vlákna dojdou na konec paralelního bloku (řádka 8 ve verzi pro C nebo direktiva end parallel ve Fortranu), dojde k zániku všech vláken s výjimkou hlavního, které dále pokračuje v vykonávání zbytku programu. Hlavní vlákno přitom na konci paralelního bloku čeká, než zaniknou ostatní vlákna. V nejjednodušším případě se spouští tolik vláken, kolik má počítač procesorů resp. jader. Počet vláken je však samozřejmě možné ovlivnit a to buď pomocí vhodných direktiv, volání knihovních funkcí, nebo pomocí nastavení systémových proměnných. Tomu se budeme ale věnovat později. K direktivě parallel lze přidat několik klauzulí, které upřesňují chování této direktivy. Jedná se o if(scalar-expression) private(list) firstprivate(list) default(shared none) shared(list) copyin(list) reduction(operator: list) num_threads(integer-expression) Klauzule if způsobí to, že překladač vytvoří dvě varianty bloku příkazů, jednu paralelní a jednu sériovou. Na základě podmínky uvedené v závorce za if se vykonává buď paralelní verze (je-li podmínka splněna), nebo sériová verze. Předpokládejme například, že pracujeme s polem o velikosti n prvků. Pro malé hodnoty n je režie spojená se vznikem a zánikem vláken často vyšší, než zisk a proto se paralelizace nevyplácí. Naproti tomu pro velká n (řekněme větší než 100) může být paralelizace vhodná. V tom případě lze použít direktivu 1 Může se stát, že výpisy budou na obrazovce různě "pomíchané". 21

1 #include <s t d i o. h> 2 #include <math. h> 3 4 int main ( ) { 5 int j ; 6 7 #pragma omp parallel private ( j ) 8 { 9 j = 0 ; 10 j = j + 1 ; 11 p r i n t f ( "%i \n ", j ) ; 12 } 13 14 return 0 ; 15 } Výpis 4.3: program openmp/private.c #pragma omp parallel if(n>100) Klauzule private se seznamem proměnných říká překladači, že má v paralelním bloku vytvořit pro každé vlákno soukromou kopii těchto proměnných. Představme si příklad 4.3. Kdybychom nepoužili klauzuli private, byla proměnná j ve sdílené paměti, tj. společná pro obě vlákna a například pro dvě vlákna by mohl program probíhat zhruba takto: Vlákno 0 Vlákno 1 j := 0 načti j přičti 1 ulož j j := 0 tiskni j načti j přičti 1 ulož j tiskni j To znamená, že vlákno 0 by mohlo vytisknout číslo 0! Všechno by záleželo pouze na načasování běhu jednotlivých vláken a výsledek by byl prakticky nepredikovatelný. Naproti tomu klauzule private(j) vytvoří pro každé vlákno novou kopii proměnné j (řekněme j_0 a j_1) a uvnitř paralelního bloku pracuje s těmito kopiemi. Tedy Vlákno 0 Vlákno 1 vytvoř j_0 j_0 := 0 načti j_0 vytvoř j_1 přičti 1 ulož j_0 j_1 := 0 tiskni j_0 načti j_1 přičti 1 zruš j_0 ulož j_1 tiskni j_1 zruš j_1 22

V tomto případě obě vlákna vytisknou výsledek 1. Je však třeba si uvědomit následující skutečnosti: privátní proměnná nemá na začátku paralelního bloku definovanou hodnotu, na konci bloku privátní proměnná zaniká a její hodnota je ztracena. Klauzule shared je opakem klauzule private. Způsobí to, že proměnné zapsané v závorce za klauzulí budou uložené ve sdílené paměti. Poznamenejme, že v C/C++ jsou všechny proměnné deklarované před vstupem do paralelního bloku standardně uvažovány jako shared (pokud se na ně nevztahují jiná pravidla). Proměnné deklarované uvnitř paralelního bloku (buď lokální proměnné uvnitř volaných funkcí nebo proměnné deklarované uvnitř bloku v C++) jsou naprotitomu typu private. Ve Fortranu platí pouze to, že lokální proměnné ve volaných procedurách a funkcích jsou (pokud není uvedeno jinak) private. Klauzule default říká, jak mají být uvažovány proměnné, které nebyly explicitně uvedeny v private nebo shared. V C/C++ jsou přípustné dvě varianty a to default(private) a default(none). Význam první z nich je zřejmý. Druhá varianta způsobí v případě, že nějaká proměnná není explicitně uvedena v shared nebo private, chybu při překladu. Ve Fortranu je k dispozici navíc varianta default(shared). Klauzule firstprivate je určitým rozšířením klauzule private. Vlákna si vytvoří privátní proměnné, do nich však na začátku běhu vlákna překopíruje hodnoty z globálních proměnných odpovídajících jmen. Následující část programu 1 int j, k ; 2 j = 1 ; 3 #pragma omp parallel firstp rivate ( j ) private (k) 4 { 5 k = j + 1 6... 7 } by bylo možné pro dvě vlákna schematicky zapsat jako j = 1; Vlákno 0 Vlákno 1 Vytvoř j_0 Vytvoř j_1 j_0 = j j_1 = j k = j_0 + 1 k = j_1 + 1...... Klauzule reduction vytvoří privátní proměnné, na začátku paralelního bloku je vhodně inicializuje a na konci bloku provede předepsanou redukci mezi sdílenou proměnnou odpovídajícího jména a privatnámi proměnnými. V C/C++ může redukcí být +,,, &,, &&,. Ve Fortranu je to +,,,.and.,.or.,.eqv.,.neqv., max, min, iand, ior, ieor. 1 int j, k ; 2 j = 1 ; 3 #pragma omp parallel reduction(+: j ) 4 { 5 j = 1 ; 6... 7 } 23

1 #include <s t d i o. h> 2 3 int main ( ) { 4 5 #pragma omp p a r a l l e l num_threads(10) 6 { 7 p r i n t f ( " Ahoj\n " ) ; 8 } 9 10 return 0 ; 11 } Výpis 4.4: program openmp/parallel-nt.c by bylo možné pro dvě vlákna schematicky zapsat jako j = 1; Vlákno 0 Vlákno 1 Vytvoř j_0 Vytvoř j_1 j_0 = 1 j_1 = 1...... j = j + j_0 j = j + j_1 Klauzule num threads umožňuje určit, na kolik vláken se má program rozštěpit, viz příklad 4.4, kde paralelní blok vykonává vždy 10 vláken. Klauzule copyin proměnných. je obdobou klauzule firstprivate, avšak týká se externích proměnných 4.2.2 Direktiva for/do Jak již bylo řečeno, direktiva parallel způsobí rozštěpení programu na jednotlivá vlákna. Tato však vykonávají stejný úsek programu. K tomu, abychom vykonávanou práci mezi jednotlivá vlákna rozdělili, však slouží jiné direktivy. Jednou z významných direktiv je direktiva rozdělující iterace v cyklu for (ve Fortranu do). Ta způsobí, že každé vlákno bude provádět pouze určitou část cyklu. Jednoduchá ukázka paralelního cyklu je ve výpisu 4.5. Nejprve dojde k rozštěpení na několik vláken a poté každé vlákno zpracuje určitou část cyklu. Pro dvě vlákna by vlákno 0 mohlo zpracovat iterace pro i = 0 až 4 a vlákno 1 iterace pro i = 5 až 9. Direktiva for resp. do může být opět doplněna následujícími klauzulemi: private(list) firstprivate(list) lastprivate(list) reduction(operator: list) ordered schedule(kind[, chunk_size]) nowait Některé klauzule mají shodný význam jako pro direktivu parallel. Přibývá zde však 24

1 #include <s t d i o. h> 2 3 int main ( ) { 4 5 int i ; 6 int x [ 1 0 ] ; 7 8 #pragma omp p a r a l l e l 9 { 10 #pragma omp for 11 for ( i =0; i <10; i ++) 12 x [ i ] = 2 i ; 13 } 14 15 return 0 ; 16 } Výpis 4.5: program openmp/for.c Klauzule ordered zajistí, že část cyklu označená direktivou ordered (viz později), se bude provádět ve stejném pořadí, jako u sériové verzie programu. Klauzule schedule určuje, jakým způsobem se budou rozdělovat iterace mezi jednotlivá vlákna. K dispozici jsou následující varianty: schedule(static) - iterace jsou rozděleny na zhruba stejně dlouhé úseky jejichž počet je dán počtem vláken. Tyto jsou pak postupně přiřazeny jednotlivým vláknům. Máme-li např. 12 iterací a tři vlákna, pak vlákno 0 bude zpracovávat iterace 0, 1, 2, 3, vlákno 1 iterace 4, 5, 6, 7 a vlákno 2 iterace 8, 9, 10, 11. Je možné použít též variantu se zadanou délkou úseku, např. schedule(static,3). Pak se iterace rozdělí na skupinku po zadané délce a ty se přidělí vláknům. Tedy pro 12 iterací a 3 vlákna by vlákno 0 zpracovávalo iterace 0, 1, 2, 9, 10, 11, vlákno 1 iterace 3, 4, 5, vlákno 2 iterace 6, 7, 8. schedule(dynamic) iterace jsou vláknům přidělovány v pořadí, v jakém je vlákna požadují. V případě, že je zadaána velikost úseku, tedy např. schedule(dynamic,2), jsou iterace opět přiřazovány po úsecích 9jinak se uvažuje délka úseku 1). Jakmile je vláknu přiřazen nějaký usek iterací, vlákno zahájí jeho zpracovaní. Po dokončení si vyžádá od scheduleru další úsek. schedule(guided) iterace jsou opět přiřazovány na základě požadavků jednotlivých vláken (tedy podobně jako u dynamic), délka přiřazovaného úseku však není konstantní a počítá se jako počet nepřiřazených iterací děleno počtem vláken. To znamená, že délka úseku exponencielně klesá. V případě, že je uvedena délka useku, tj. např. schedule(guided,5), budou přiřazovány úseky tak, aby jejich délky (s výjimkou posledního) nebyly menší než zadaná délka. runtime aktualní schedule je určena až při běhu programu pomocí systémových proměnných. Poznamenejme, že standard neříká, jakým způsobem se řeší případ, kdy počet iterací není dělitelný počtem vláken. Tyto situace jsou závislé na implementaci překladače. Klauzule nowait vlákna na sebe po ukončení cyklu standardně čekají. Pokud však uvedeme klauzuli nowait, vlákna opustí paralelní cyklus c okamžiku, kdy dokončí poslední přidělený úsek iterací. 25

Další direktivy OpenMP obsahuje samozřejmě ještě další direktivy, nicméně výše uvedené direktivy jsou pro jednoduché paralelní programování zaměřené na práci s poli ve většině případů nejvhodnější. 26

Kapitola 5 Dodatek 5.1 Charakteristika počítačů použitých pro měření 5.2 Program pro měření času 27

Výpis 5.1: program cache/timer.hpp 1 #i f n d e f TIMER_H 2 #define TIMER_H 3 4 #include <s t r i n g > 5 #include <sys / time. h> 6 #include <s y s / r e s o u r c e. h> 7 8 9 c l a s s Timer { 10 p r i v a t e : 11 s t d : : s t r i n g name ; 12 double walltime ; 13 double usertime ; 14 long numberofruns ; 15 bool isrunning ; 16 double lastwalltime ; 17 double lastusertime ; 18 19 double systemusertime ( ) { 20 struct rusage rusage ; 21 g e t r u s a g e (RUSAGE_SELF, &r u s a g e ) ; 22 return ( double ) ( rusage. ru_utime. tv_sec ) + 23 ( double ) ( r u s a g e. ru_utime. tv_usec ) 1. 0 e 06; 24 } 25 26 double systemwalltime ( ) { 27 struct timeval timeval ; 28 gettimeofday (& timeval, 0 ) ; 29 return ( double ) ( t i m e v a l. tv_sec ) + ( double ) ( t i m e v a l. tv_usec ) 1. 0 e 06; 30 } 31 32 p u b l i c : 33 34 35 Timer ( ) : numberofruns ( 0 ), walltime ( 0 ), usertime ( 0 ), 36 isrunning ( f a l s e ), name ( " unnamed " ) { } ; 37 38 Timer ( s t d : : s t r i n g name ) : numberofruns ( 0 ), walltime ( 0 ), usertime ( 0 ), 39 isrunning ( f a l s e ), name ( name ) { } ; 40 41 ~ Timer ( ) { 42 std : : cout << "TIMER " << name << " : " 43 " u s e r time : " << usertime << " s " << 44 " w a l l time : " << walltime << " s " << 45 " #c a l l s : " << numberofruns << s t d : : e n d l ; 46 } 47 48 void s t a r t ( ) { 49 isrunning = true ; 50 lastwalltime = systemwalltime ( ) ; 51 lastusertime = systemusertime ( ) ; 52 } 53 54 void s t o p ( ) { 55 i f ( isrunning ) { 56 isrunning = f a l s e ; 57 usertime += systemusertime ( ) lastusertime ; 58 walltime += systemwalltime ( ) lastwalltime ; 59 numberofruns++; 60 } 61 } 62 63 double getwalltime ( ) { 64 i f ( isrunning ) 65 return systemwalltime ( ) lastwalltime ; 66 e l s e 67 return walltime ; 68 } 69 70 } ; 71 72 #endif 28

CPU Pentium-M Core 2 Duo Itanium2 Frekvence [MHz] 1500 2400 1500 Velikost [kb] a délka řádky cache [B] L1 32/64 32/64 16/64 L2 2048/64 4096/64 256/64 L3 6144/128 Latence [ns] L1 2 1 1 L2 7 6 4 L3 13 RAM 110 90 130 Propustnost čtení/zápis [GB/s] L1 22.9/22.9 L2 12.1/4.8 L3 RAM 2.4/0.6 5.4/2.1 0.8/1.7 Tabulka 5.1: Charakteristiky počítačů použitých pro měření 29