A Fortran programozási nyelv

Párhuzamosság



A FORTRAN-nak MIMD (Multiple Instruction stream Multiple Data stream, egymástól független processzorok által támogatott párhuzamosság) és SIMD (Single Instruction Multiple Data, vektorprocesszorokban használt párhuzamos technika, de ilyen az MMX, 3DNow! és SSE is) típusú párhuzamos programozást lehetővé tevő változata létezik.

MIMD

MIMD programokat írhatunk a PVM 3.1 programcsomag felhasználásával, Fortran 77 nyelven. A PVM egy párhuzamos programok írását támogató függvénykönyvtár. A PVM függvények a következő csoportokba oszthatók: folyamat vezérlés, információ lekérdezés, dinamikus konfigurálás, dinamikus folyamat csoportok kezelése, üzenet pufferek, szignálok, üzenetküldés, üzenet fogadás, hibakezelés.

Alább láthatók a PVM 3.1 Fortran felületének függvényei.

Process Control pvmfmytid( tid ) pvmfexit( info ) pvmfkill( tid, info ) pvmfspawn( task,flag,where,ntask,tids,numt ) Information pvmfparent( tid ) pvmfpstat( tid, pstat ) pvmfmstat( host, mstat ) pvmfconfig( nhost, narch, info ) pvmftasks( which, ntask, info ) Dynamic Configuration pvmfaddhost( host, info ) pvmfdelhost( host, info ) Dynamic Process Groups pvmfjoingroup( group, inum ) pvmflvgroup( group, info ) pvmfgsize( group, size, info ) pvmfgettid( group, inum, tid ) pvmfgetinst( group, tid, inum ) pvmfbarrier( group, count, info ) pvmfbcast( group, msgtag, info ) Message Buffers pvmfmkbuf( encoding, bufid ) pvmffreebuf( bufid, info ) pvmfgetsbuf( bufid ) pvmfgetrbuf( bufid ) pvmfsetsbuf( bufid, oldbuf ) pvmfsetrbuf( bufid, oldbuf 0 pvmfinitsend( encoding, bufid ) Signalling pvmfsendsig( tid, signum, info ) pvmfnotify( about, msgtag, ntask, tids, info ) Sending pvmfpack( what, xp, nitem, stride, info ) pvmfsend( tid, msgtag, bufid ) pvmfmcast( ntask, tids, msgtag, info ) Receiving pvmfrecv( tid, msgtag, bufid ) pvmfprobe( tid, msgtag, bufid ) pvmfnrecv( tid, msgtag, bufid ) pvmfbufinfo( bufid, bytes, msgtag, tid, info) pvmfunpack( what, xp, nitem, stride, info ) Error Handling pvmfperror( msg, info ) pvmfserror( set, oldset )

SIMD

SIMD programok jellemzője, hogy nagy adatmennyiségen kell ugyanazt a műveletsorozatot (lehetőleg párhuzamosan) elvégezni. A program szekvenciálisan és párhuzamosan végrehajtható részekre bontható. A FORTRAN 90 és a HPF lehetővé teszi adatpárhuzamos FORTRAN programok írását.

Alapvető újdonság a tömbműveletek és tömboperátorok megjelenése. Összeadhatunk, összeszorozhatunk azonos dimenziójú tömböket, illetve tömbök részeivel is végezhetünk aritmetikai műveleteket.

Tömb műveletek

A Fortran 90 nyelv alapvető nyelvi újdonsága - amely a párhuzamosság bevezetését lehetővé tette - a tömbökkel végzett műveletek egyszerűbb írásmódja.

Az F77-ben megszokott skalár műveleteket a Fortran 90-ben már tömbökre is lehet alkalmazni. Az így leírt műveleteket a fordító a kifejezésben szereplő tömbök megfelelő elemeire fogja alkalmazni. Az egyértelmű szemantika érdekében a műveletekben szereplő tömbök dimenzióinak és méreteinek meg kell egyezniük.

A következő példában a B tömb megfelelő elemeinek és c változónak az összege kerül bele A tömb megfelelő elemeibe. A beépített függvények - mint a gyökvonás - is használhatóak.

integer A(10,10), B(10,10), c A = B + c A = A + 1.0 A = SQRT(A)

A tömbműveleteknél a kifejezésekbe persze nem csak egész tömbök, hanem azok részei is behelyettesíthetőek. Résztömböt a megfelelő koordináta helyére írt számhármassal lehet megadni: alsó-határ:felső-határ:lépésköz. A tömb lépésköznyi távolságra lévő elemei az alsó határtól a felső határig lépkedve lesznek a résztömb elemei. Ilyen specifikációt hiányosan és több koordináta helyén is meg lehet adni.

integer A(4,8), B(4,8) A(2, :) ! A masodik sora A(2, 2:7) ! A masodik soranak kozepe A(2, 1:8:2) ! A masodik sorabol minden ! masodik elem ! B tomb eltolasa, es hozzaadasa A-hoz A(:, 1:7) = A(:, 1:7) + B(:, 2:8)

Tömb operátorok

A Fortran 90 nyelv egy argumentumú beépített műveletek - mint a gyökvonás - alkalmazását tömbökre is megengedi. Ebben az esetben az operátort a program a tömb minden elemére külön-külön végrehajtja. Ilyen pl.: SQRT, SIN, TAN, ABS ...

Vannak persze olyan műveletek is, amelyek egész tömböket is érintenek. Ezek közül a következőek vannak definiálva:

Az előbb felsorolt függvények használata lehetővé teszi, hogy a fordító felismerje a párhuzamosítható részeket. A hatékonyság érdekében szükség lehet arra, hogy az adatok szétosztását a programozó befolyásolhassa. Mátrix deklarációkban rendelkezhetünk az adatok általunk javasolt szétosztásáról. (Különben a fordító dönt.)

Az összeg, szorzat, minimum és maximum műveletek az egész tömbre értendők, tekintet nélkül annak alakjára, dimenzióinak számára.

A maximum és minimum helyek keresésekor az eredményt az argumentum tömb dimenziószámának megfelelő nagyságú tömbbe fogja beletenni az operátor:

integer A(10,10), B(20, 10), P(2), s ... P = MAXLOC(A) s = SUM(A) B(11:20, :) = MATMUL(A, B(1:10, :)

Egyes műveleteknél szükség lehet a szomszédos mátrix elemekre. Ilyen esetekben használható a CSHIFT művelet, mellyel a mátrixot valamelyik dimenzió irányában körkörösen el lehet forgatni:

result = CSHIFT(X, 2, -1) ! masodik dimenzioban egy ! elemmel visszaforgat 1 2 3 3 1 2 4 5 6 -> 6 4 5 7 8 9 9 7 8 CSHIFT(X, 2, -1) result = CSHIFT(X, -1) ! elso dimenzional eleg ! egy parameter 1 2 3 7 8 9 4 5 6 -> 1 2 3 7 8 9 4 5 6 CSHIFT(X, -1)

A következő átlagolási feladat az eltolást használva sokkal egyszerűbben leírható:

real X(0:99), B(0:99) do i = 0,99 B(i) = ( X(mod(i+99,100) + X(mod(i+1,100)) )/2 enddo

F90-ben:

real X(100), B(100), L(100), R(100) L = CSHIFT(X,+1) R = CSHIFT(X,-1) B = ( L + R )/2

vagy még egyszerűbben:

real X(100), B(100) B = ( CSHIFT(X,+1) + CSHIFT(X,-1) )/2

A HPF, illetve más párhuzamos Fortran kiterjesztések ezeken kívül még operátorok gazdag választékát nyújtják, például asszociatív műveletek részeredményeit is kiszámoló eszközöket.

Explicit párhuzamosságot támogató konstrukciók a WHERE, FORALL, INDEPENDENT és PURE. A WHERE segítségével egy tömb minden elemére végrehajtandó utasítás alternatívát programozhatunk. A FORALL a párhuzamosan végrehajtott ciklus kulcsszava, az INDEPENDENT hagyományos ciklus párhuzamosítására való. A PURE direktívával definiált függvények hívhatók több példányban elindítható programrészekből. Egy PURE függvényben nem lehet globális változóra hivatkozni, és a paraméterek értékét megváltoztatni.

OpenMP programozói környezet

Az OpenMP a fő informatikai cégek által definiált interfész, hordozható, skálázható eszközt nyújt a fejlesztők számára elosztott-memória modellel. Az API C/C++ és Fortran nyelveket támogatja számos architektúrán: többek között UNIX és Windows.

Mi is az OpenMP?

Linkek:

Jelenlegi változat: OpenMP 3.0

OpenMP programozói modell

Példa egy OpenMP kódszerkezetre

PROGRAM HELLO INTEGER VAR1, VAR2, VAR3 Szekvencialis kod . . . A parhuzamos feldolgozas kezdete-több szál indítása. A változok ervenyessege !$OMP PARALLEL PRIVATE(VAR1, VAR2) SHARED(VAR3) Parhuzamosan vegrehajtando minden szal eseten . . . Miden szal kilepo szinkronizacios pontja. !$OMP END PARALLEL Szekvencialis vegrehajtas. . . . END

Párhuzamos régió létrehozása

Célja egy blokk kijelölése párhuzamos végrehajtásra

Formátum:

!$OMP PARALLEL [clause ...] IF (scalar_logical_expression) PRIVATE (list) SHARED (list) DEFAULT (PRIVATE | FIRSTPRIVATE | SHARED | NONE) FIRSTPRIVATE (list) REDUCTION (operator: list) COPYIN (list) NUM_THREADS (scalar-integer-expression) block !$OMP END PARALLEL

A "Hello World" példaprogram

PROGRAM HELLO INTEGER NTHREADS, TID, OMP_GET_NUM_THREADS, + OMP_GET_THREAD_NUM C Szalak letrehozasa TID azonosoitoval !$OMP PARALLEL PRIVATE(TID) C Obtain and print thread id TID = OMP_GET_THREAD_NUM() PRINT *, 'Hello World from thread = ', TID C Only master thread does this IF (TID .EQ. 0) THEN NTHREADS = OMP_GET_NUM_THREADS() PRINT *, 'Number of threads = ', NTHREADS END IF C Minden szal itt talalkozik es megszunik !$OMP END PARALLEL END

1. Feladat-kiosztás DO direktívával

Egyszerű vektor összeadó program

PROGRAM VEC_ADD_DO INTEGER N, CHUNKSIZE, CHUNK, I PARAMETER (N=1000) PARAMETER (CHUNKSIZE=100) REAL A(N), B(N), C(N) ! Some initializations DO I = 1, N A(I) = I * 1.0 B(I) = A(I) ENDDO CHUNK = CHUNKSIZE !$OMP PARALLEL SHARED(A,B,C,CHUNK) PRIVATE(I) !$OMP DO SCHEDULE(DYNAMIC,CHUNK) DO I = 1, N C(I) = A(I) + B(I) ENDDO !$OMP END DO NOWAIT !$OMP END PARALLEL END

2. Feladat-kiosztás SECTIONS direktívával

Minden egyes így kijelölt blokkot egy szál hajt végre-különböző blokkok külön szálakhoz.

PROGRAM VEC_ADD_SECTIONS INTEGER N, I PARAMETER (N=1000) REAL A(N), B(N), C(N), D(N) ! Some initializations DO I = 1, N A(I) = I * 1.5 B(I) = I + 22.35 ENDDO !$OMP PARALLEL SHARED(A,B,C,D), PRIVATE(I) !$OMP SECTIONS !$OMP SECTION DO I = 1, N C(I) = A(I) + B(I) ENDDO !$OMP SECTION DO I = 1, N D(I) = A(I) * B(I) ENDDO !$OMP END SECTIONS NOWAIT !$OMP END PARALLEL END

3. Feladat kiosztás WORKSHARE direktívával

A kijelölt blokk csak a következőt tartalmazhatja:

PROGRAM WORKSHARE INTEGER N, I, J PARAMETER (N=100) REAL AA(N,N), BB(N,N), CC(N,N), DD(N,N), FIRST, LAST ! Some initializations DO I = 1, N DO J = 1, N AA(J,I) = I * 1.0 BB(J,I) = J + 1.0 ENDDO ENDDO !$OMP PARALLEL SHARED(AA,BB,CC,DD,FIRST,LAST) !$OMP WORKSHARE CC = AA * BB DD = AA + BB FIRST = CC(1,1) + DD(1,1) LAST = CC(N,N) + DD(N,N) !$OMP END WORKSHARE NOWAIT !$OMP END PARALLEL END

Szinkronizáció CRITICAL direktívával

Ha egy futó szál a kritikus blokkban van, akkor egy másik szál várakozik a belépési pontnál mindaddig, amíg az előző szál el nem hagyja a kijelölt szakaszt.

PROGRAM CRITICAL INTEGER X X = 0 !$OMP PARALLEL SHARED(X) !$OMP CRITICAL X = X + 1 !$OMP END CRITICAL !$OMP END PARALLEL END

A THREADPRIVATE direktíva

A direktíva a változó deklaráció után következik. Minden egyes szál saját másolatot kap ezen változókról, mely nem elérhető máshonnan.

PROGRAM THREADPRIV INTEGER A, B, I, TID, OMP_GET_THREAD_NUM REAL*4 X COMMON /C1/ A !$OMP THREADPRIVATE(/C1/, X) C Explicitly turn off dynamic threads CALL OMP_SET_DYNAMIC(.FALSE.) PRINT *, '1st Parallel Region:' !$OMP PARALLEL PRIVATE(B, TID) TID = OMP_GET_THREAD_NUM() A = TID B = TID X = 1.1 * TID + 1.0 PRINT *, 'Thread',TID,': A,B,X=',A,B,X !$OMP END PARALLEL PRINT *, '************************************' PRINT *, 'Master thread doing serial work here' PRINT *, '************************************' PRINT *, '2nd Parallel Region: ' !$OMP PARALLEL PRIVATE(TID) TID = OMP_GET_THREAD_NUM() PRINT *, 'Thread',TID,': A,B,X=',A,B,X !$OMP END PARALLEL END Output: 1st Parallel Region: Thread 0 : A,B,X= 0 0 1.000000000 Thread 1 : A,B,X= 1 1 2.099999905 Thread 3 : A,B,X= 3 3 4.300000191 Thread 2 : A,B,X= 2 2 3.200000048 ************************************ Master thread doing serial work here ************************************ 2nd Parallel Region: Thread 0 : A,B,X= 0 0 1.000000000 Thread 2 : A,B,X= 2 0 3.200000048 Thread 3 : A,B,X= 3 0 4.300000191 Thread 1 : A,B,X= 1 0 2.099999905

REDUCTION klauzula

A listában felsorolt változóról egy másolat készül minden szál esetén. A kijelölt blokk után az eredmény a globális osztott memóriahelyre kerül.

PROGRAM DOT_PRODUCT INTEGER N, CHUNKSIZE, CHUNK, I PARAMETER (N=100) PARAMETER (CHUNKSIZE=10) REAL A(N), B(N), RESULT ! Some initializations DO I = 1, N A(I) = I * 1.0 B(I) = I * 2.0 ENDDO RESULT= 0.0 CHUNK = CHUNKSIZE !$OMP PARALLEL DO !$OMP& DEFAULT(SHARED) PRIVATE(I) !$OMP& SCHEDULE(STATIC,CHUNK) !$OMP& REDUCTION(+:RESULT) DO I = 1, N RESULT = RESULT + (A(I) * B(I)) ENDDO !$OMP END PARALLEL DO NOWAIT PRINT *, 'Final Result= ', RESULT END

Megszorítások

NOWAIT direktíva

Bizonyos körülmények között nem szükséges minden futó szálnak befejeződnie a program adott pontján- a következő művelet nem függ az előzőektől.
Az ilyen esetekben kihagyható az implicit szinkronizáció:

!$OMP PARALLEL !$OMP DO do i = 1, 1000 a = i enddo !$OMP END DO NOWAIT !$OMP DO do i = 1, 1000 b = i enddo !$OMP END DO !$OMP END PARALLEL

SCHEDULE(type, chunk) direktíva

Ha egy do-loop ciklus minden egyes iterációját szeretnénk több szál között kiosztani, akkor legegyszerűbb azonos számú iterációt hozzárendelni a szálakhoz. Ezt valósítja meg a STATIC kiosztás:

!$OMP DO SCHEDULE(STATIC, chunk) do i = 1, 600 ... enddo !$OMP END DO

Az opcionális chunk paraméter a kiosztott iterációk mennyiségét határozza meg, de alapértelmezésben azonos az egyes szálak között.

Ha az egyes iterációk nem homogének, akkor a dinamikus hozzárendelést érdemes választani:

!$OMP DO SCHEDULE(DYNAMIC,256) do i = 1, 10230 ... enddo !$OMP END DO

Az opcionális chunk paraméter az iterációk számát adja meg, alapértelmezésben egyet szálanként.
Ha az adott szál befejezte a ciklus törzsét, akkor egy újabb iterációs blokkot kap.

Az előző módszer a statikus kiosztáshoz képest nagyobb teljesítményre képes, viszont járulékos számítást igényel a különböző iterációk kiosztásánál.

A másik megközelítés egyre kisebb egységeket oszt ki az egyes szálak között:

!$OMP DO SCHEDULE(GUIDED,256) do i = 1, 10230 ... enddo !$OMP END DO

A kiosztott iterációk számát felezi az OpenMP futattási környezet, ahol a chunk paraméter a legkisebb egységet határozza meg.

A fent említett módszerek még fordítási időben határozzák meg a szálak kiosztását, de lehetőség van ezt futási időben is megtenni:

$OMP DO SCHEDULE(RUNTIME)

Ebben az esetben az OMP_SCHEDULE környezeti változó értéke határozza meg a kiosztást.

ORDERED direktíva

Ha az iterációt a megadott sorrendben kell végrehajtani, akkor ezt előírhatjuk az !$OMP ORDERED/!$OMP END ORDERED direktívapárral:

!$OMP DO ORDERED do i = 1, 1000 !$OMP ORDERED A(i) = 2 * A(i-1) !$OMP END ORDERED enddo !$OMP END DO

Az OpenMP futtatási környezet könyvtári rutinjai

OMP_set_num_threads szubrutin

Ez a szubrutin a végrahajtási szálak számát határozza meg a következő párhuzamos blokkban:

subroutine OMP_set_num_threads(number_of_threads) integer(kind = OMP_integer_kind), intent(in) :: number_of_threads end subroutine OMP_set_num_threads

Ha a szálak dinamikus kiosztása megengedett, akkor ez a maximális , egyébként pedig a futtatható szálakat adja meg.

OMP_get_num_threads szubrutin

Ez a függvény a jelenleg futó szálak számát adja vissza, illetve egyet szekvenciális blokk esetén:

function OMP_get_num_threads() integer(kind = OMP_integer_kind) :: OMP_get_num_threads end function OMP_get_num_threads

OMP_get_max_threads szubrutin

Ez a függvény a maximális elérhető szálak számát adja vissza, ami különbözhet az előző függvény értékétől dinamikus kiosztás esetén:

function OMP_get_max_threads() integer(kind = OMP_integer_kind) :: OMP_get_max_threads end function OMP_get_max_threads

OMP_get_thread_num szubrutin

A szál azonosítóját adja vissza.A szálak 0-tól (mester) vannak számozva.

function OMP_get_thread_num() integer(kind = OMP_integer_kind) :: OMP_get_thread_num end function OMP_get_thread_num

OMP_getnum_procs szubrutin

Az elérhető processzorok számát adja vissza:

function OMP_get_num_procs() integer(kind = OMP_integer_kind) :: OMP_get_num_procs end function OMP_get_num_procs

OMP_set_dynamic szubrutin

A szubrutin tiltja vagy engedélyezi a dinamikus szálkiosztást:
subroutine OMP_set_dynamic(enable) logical(kind = OMP_logical_kind), intent(in) :: enable end subroutine OMP_set_dynamic

Ha a megadott érték .TRUE., akkor a futtató környezet dinamikusan kezeli a szálakat az adott SMP architektúrán. Az alapbeállítás implementációként változik.

OMP_get_dynamic szubrutin

Ez a függvény visszaadja a dinamikus szálkiosztás beállítását:
function OMP_get_dynamic() logical(kind = OMP_logical_kind) :: OMP_get_dynamic end function OMP_get_dynamic

OMP_set_nested szubrutin

Ez a függvény megengedi/tiltja a beágyazott többszálú feldolgozást:
subroutine OMP_set_nested(enable) logical(kind = OMP_logical_kind), intent(in) :: enable end subroutine OMP_set_nested

Alapbeállításként .FALSE., vagyis a futtató környezet sorosan dolgozza fel a belső blokkokat. A szálak száma implementáció-függő.

OMP_get_nested szubrutin

Ez a függvény az OMP_set_nested függvény lekérdező párja:
function OMP_get_nested() logical(kind = OMP_logical_kind) :: OMP_get_nested end function OMP_get_nested

Zároló függvények

A zárolás jelzőként értelmezheő, amely ki/be kapcsolható. Minden szál ellenőrheti a zárolás állapotát. Az állapotot beállító szál a zárolás tulajdonosa.
Ez addig érvényes, amíg ki nem kapcsolja a zárat az adott szál. A mechanizmus abban különbözik az előzőekben felvázolt, hogy a lekérdező szálaknak nem kell figyelmbe venniük a zárolást, amely flexibilis kódot eredményez.

program Main use omp_lib implicit none integer(kind = OMP_lock_kind) :: lck integer(kind = OMP_integer_kind) :: ID call OMP_init_lock(lck) !$OMP PARALLEL SHARED(LCK) PRIVATE(ID) ID = OMP_get_thread_num() do while(.not.OMP_test_lock(lck)) ... !dolgozz addig, amig várakozni kell enddo ... !feladat amit a zarolas alatt kell elvegezni !$OMP END PARALLEL call OMP_destroy_lock(lck) end program Main

Ebben a példában minden szál, amely nem birtokolja a zárat, más feladatot tud végrehajtani.

A következő kódrészlet a beágyazott zárolásra ad példát:

module data_types use omp_lib, only: OMP_nest_lock_kind implicit none type number integer :: n integer(OMP_nest_lock_kind) :: lck end type number end module data_types !---------------------------------------------------------! program Main use omp_lib use data_types implicit none type(number) :: x x%n = 0 call OMP_init_lock(x%lck) !$OMP PARALLEL SECTIONS SHARED(x) !$OMP SECTION call add(x,20) !$OMP SECTION call substract(x,10) !$OMP END PARALLEL call OMP_destroy_lock(lck) end program Main !---------------------------------------------------------! subroutine add(x,d) use omp_lib use data_types implicit none type(number) :: x integer :: d call OMP_set_nest_lock(x%lck) x%n = x%n + d call OMP_unset_nest_lock(x%lck) end subroutine add