OpenMP - OpenMP

OpenMP
Logo OpenMP
Autori originali OpenMP Architecture Review Board
Dezvoltatori OpenMP Architecture Review Board
Versiune stabila
5.1 / 13 noiembrie 2020 ; 9 luni in urma ( 13.11.2020 )
Sistem de operare Cross-platform
Platformă Cross-platform
Tip Extensie la C , C ++ și Fortran ; API
Licență Variat
Site-ul web openmp .org

OpenMP ( Open Multi-Processing ) este o interfață de programare a aplicațiilor (API) care acceptă programare multiprocesare multipartiment cu memorie partajată în C , C ++ și Fortran , pe multe platforme, arhitecturi de seturi de instrucțiuni și sisteme de operare , inclusiv Solaris , AIX , HP-UX , Linux , macOS și Windows . Acesta constă dintr-un set de directive de compilare , rutine de bibliotecă și variabile de mediu care influențează comportamentul în timpul rulării.

OpenMP este administrat de consorțiul tehnologic nonprofit OpenMP Architecture Review Board (sau OpenMP ARB ), definit împreună de o gamă largă de furnizori de hardware și software de computer, inclusiv Arm , AMD , IBM , Intel , Cray , HP , Fujitsu , Nvidia , NEC , Red Hat , Texas Instruments și Oracle Corporation .

OpenMP utilizează un model portabil , scalabil, care oferă programatorilor o interfață simplă și flexibilă pentru dezvoltarea de aplicații paralele pentru platforme care variază de la computerul desktop standard la supercomputer .

O aplicație construită cu modelul hibrid de programare paralelă poate rula pe un cluster de calculator utilizând atât OpenMP, cât și Message Passing Interface (MPI), astfel încât OpenMP este utilizat pentru paralelism într- un nod (multi-core), în timp ce MPI este utilizat pentru paralelism între noduri . De asemenea, s-au depus eforturi pentru a rula OpenMP pe sisteme distribuite de memorie partajată software , pentru a traduce OpenMP în MPI și pentru a extinde OpenMP pentru sistemele de memorie non-partajate.

Proiecta

O ilustrare a multithreading în care firul primar elimină un număr de fire care execută blocuri de cod în paralel.

OpenMP este o implementare a multithreading-ului , o metodă de paralelizare prin care un fir primar (o serie de instrucțiuni executate consecutiv) bifurcă un număr specificat de sub- fire și sistemul împarte o sarcină între ele. Executarea thread-urilor este simultană , mediul de execuție alocând thread-urile la diferite procesoare.

Secțiunea de cod care este menită să ruleze în paralel este marcată în consecință, cu o directivă de compilare care va determina formarea firelor înainte de executarea secțiunii. Fiecare fir are atașat un id care poate fi obținut folosind o funcție (numită omp_get_thread_num()). ID-ul firului este un număr întreg, iar firul principal are un id de 0 . După executarea codului paralelizat, firele se unesc din nou în firul principal, care continuă până la sfârșitul programului.

În mod implicit, fiecare fir execută secțiunea paralelizată a codului independent. Construcțiile de partajare a lucrărilor pot fi folosite pentru a împărți o activitate între fire, astfel încât fiecare fir să își execute partea alocată a codului. Atât paralelismul sarcinilor, cât și paralelismul datelor pot fi realizate folosind OpenMP în acest fel.

Mediul de execuție alocă fire de procesare în funcție de utilizare, încărcarea mașinii și alți factori. Mediul de execuție poate atribui numărul de fire pe baza variabilelor de mediu sau codul poate face acest lucru folosind funcții. Funcțiile OpenMP sunt incluse într-un fișier antet etichetat omp.h în C / C ++ .

Istorie

OpenMP Architecture Review Board (ARB) a publicat primele sale specificații API, OpenMP pentru Fortran 1.0, în octombrie 1997. În octombrie anul următor au lansat standardul C / C ++. În 2000 a apărut versiunea 2.0 a specificațiilor Fortran, iar versiunea 2.0 a specificațiilor C / C ++ a fost lansată în 2002. Versiunea 2.5 este o specificație combinată C / C ++ / Fortran care a fost lansată în 2005.

Până la versiunea 2.0, OpenMP a specificat în primul rând modalități de a paralela bucle foarte obișnuite, deoarece acestea apar în programarea numerică orientată pe matrice , unde numărul de iterații al buclei este cunoscut la momentul intrării. Aceasta a fost recunoscută ca o limitare și s-au adăugat la implementări diverse extensii paralele de sarcini. În 2005, s-a format un efort de standardizare a paralelismului sarcinilor, care a publicat o propunere în 2007, inspirându-se din caracteristicile paralelismului sarcinilor din Cilk , X10 și Chapel .

Versiunea 3.0 a fost lansată în mai 2008. În noile caracteristici din 3.0 este inclusă noțiunea de sarcini și construcția de sarcini , lărgind semnificativ domeniul de aplicare al OpenMP dincolo de construcțiile de buclă paralelă care alcătuiau majoritatea OpenMP 2.0.

Versiunea 4.0 a specificației a fost lansată în iulie 2013. Aceasta adaugă sau îmbunătățește următoarele caracteristici: suport pentru acceleratoare ; atomice ; eroare de manipulare; afinitate fir ; extensii de sarcini; reducere definită de utilizator ; Suport SIMD ; Suport Fortran 2003 .

Versiunea actuală este 5.1, lansată în noiembrie 2020.

Rețineți că nu toate compilatoarele (și sistemele de operare) acceptă setul complet de caracteristici pentru cea mai recentă versiune.

Elemente de bază

Diagrama construcțiilor OpenMP

Elementele de bază ale OpenMP sunt construcțiile pentru crearea de fire, distribuția volumului de lucru (partajarea de lucru), gestionarea mediului de date, sincronizarea firelor, rutine de rulare la nivel de utilizator și variabile de mediu.

În C / C ++, OpenMP folosește #pragme . Pragmele specifice OpenMP sunt enumerate mai jos.

Crearea firului

Paralela pragma omp este utilizată pentru a furniza fire suplimentare pentru a efectua lucrările închise în construcție în paralel. Firul original va fi notat ca fir principal cu ID-ul firului 0.

Exemplu (program C): Afișați „Bună ziua, lume”. folosind mai multe fire.

#include <stdio.h>
#include <omp.h>

int main(void)
{
    #pragma omp parallel
    printf("Hello, world.\n");
    return 0;
}

Utilizați flag -fopenmp pentru a compila folosind GCC:

$ gcc -fopenmp hello.c -o hello

Ieșire pe un computer cu două nuclee și, prin urmare, două fire:

Hello, world.
Hello, world.

Cu toate acestea, ieșirea poate fi, de asemenea, confuză din cauza stării de cursă cauzată de cele două fire care împart ieșirea standard .

Hello, wHello, woorld.
rld.

(Indiferent dacă printfeste thread-safe depinde de implementare. C ++ std::cout, pe de altă parte, este întotdeauna thread-safe.)

Construcții de partajare a muncii

Folosit pentru a specifica cum să atribuiți lucrări independente unuia sau tuturor firelor.

  • omp for sau omp do : folosit pentru a împărți iterațiile de bucle între fire, numite și constructe de bucle.
  • secțiuni : atribuirea blocurilor de cod consecutive, dar independente pentru diferite fire
  • single : specificând un bloc de cod care este executat de un singur fir, o barieră este implicită în cele din urmă
  • master : similar cu single, dar blocul de cod va fi executat numai de firul master și nu va exista nicio barieră în cele din urmă.

Exemplu: inițializați valoarea unui tablou mare în paralel, folosind fiecare fir pentru a face o parte din lucrare

int main(int argc, char **argv)
{
    int a[100000];

    #pragma omp parallel for
    for (int i = 0; i < 100000; i++) {
        a[i] = 2 * i;
    }

    return 0;
}

Acest exemplu este jenant de paralel și depinde doar de valoarea lui i . Paralela OpenMP pentru pavilion îi spune sistemului OpenMP să împartă această sarcină între firele sale de lucru. Firele vor primi fiecare o versiune unică și privată a variabilei. De exemplu, cu două fire de lucru, unui thread i se poate înmâna o versiune de i care rulează de la 0 la 49999, în timp ce al doilea primește o versiune de la 50000 la 99999.

Directivele variante

Directivele variante sunt una dintre caracteristicile majore introduse în specificațiile OpenMP 5.0 pentru a facilita programatorilor să îmbunătățească portabilitatea performanțelor. Acestea permit adaptarea pragmelor OpenMP și a codului de utilizator în timpul compilării. Specificația definește trăsăturile pentru a descrie construcțiile OpenMP active, dispozitivele de execuție și funcționalitatea oferite de o implementare, selectoarele de context bazate pe trăsături și condițiile definite de utilizator și directivele metadirective și de declarare a directivelor pentru ca utilizatorii să programeze aceeași regiune de cod cu variante de directive.

  • Metadirective este o directivă executabil care rezolvă în mod condiționat la o altă directivă la momentul compilării, selectând din directivă variante multiple bazate pe trasaturi care definesc o condiție OpenMP sau context.
  • Varianta vestim directivei are o funcționalitate similară cu metadirective , dar selectează o variantă funcție la call-site - ul în funcție de context sau condiții definite de utilizator.

Mecanismul oferit de cele două variante de directive pentru selectarea variantelor este mai convenabil de utilizat decât preprocesarea C / C ++, deoarece acceptă direct selecția variantelor în OpenMP și permite unui compilator OpenMP să analizeze și să determine directiva finală din variante și context.

// code adaptation using preprocessing directives

int v1[N], v2[N], v3[N];
#if defined(nvptx)     
 #pragma omp target teams distribute parallel loop map(to:v1,v2) map(from:v3)
  for (int i= 0; i< N; i++) 
     v3[i] = v1[i] * v2[i];  
#else 
 #pragma omp target parallel loop map(to:v1,v2) map(from:v3)
  for (int i= 0; i< N; i++) 
     v3[i] = v1[i] * v2[i];  
#endif


// code adaptation using metadirective in OpenMP 5.0

int v1[N], v2[N], v3[N];
#pragma omp target map(to:v1,v2) map(from:v3)
  #pragma omp metadirective \
     when(device={arch(nvptx)}: target teams distribute parallel loop)\
     default(target parallel loop)
  for (int i= 0; i< N; i++) 
     v3[i] = v1[i] * v2[i];

Clauze

Deoarece OpenMP este un model de programare a memoriei partajate, majoritatea variabilelor din codul OpenMP sunt vizibile în mod implicit pentru toate firele. Dar, uneori, variabilele private sunt necesare pentru a evita condițiile cursei și este necesar să se transmită valori între partea secvențială și regiunea paralelă (blocul de cod executat în paralel), astfel încât managementul mediului de date este introdus ca clauze de atribuire de partajare a datelor prin adăugarea acestora la directiva OpenMP. Diferitele tipuri de clauze sunt:

Clauze de atribut de partajare a datelor
  • partajate : datele declarate în afara unei regiuni paralele sunt partajate, ceea ce înseamnă vizibile și accesibile de toate firele simultan. În mod implicit, toate variabilele din regiunea de partajare a muncii sunt partajate, cu excepția contorului de iterație de buclă.
  • private : datele declarate într-o regiune paralelă sunt private pentru fiecare fir, ceea ce înseamnă că fiecare fir va avea o copie locală și îl va folosi ca variabilă temporară. O variabilă privată nu este inițializată și valoarea nu este menținută pentru utilizare în afara regiunii paralele. În mod implicit, contoarele de iterație de buclă din constructele de bucle OpenMP sunt private.
  • implicit : permite programatorului să afirme că scopul implicit al datelor într-o regiune paralelă va fi fie partajat , fie nici unul pentru C / C ++, fie partajat , firstprivate , privat sau nici unul pentru Fortran. Opțiunea none obligă programatorul să declare fiecare variabilă în regiunea paralelă utilizând clauzele atributului de partajare a datelor.
  • firstprivate : ca privat, cu excepția inițializării la valoarea inițială.
  • lastprivate : ca privat, cu excepția valorii originale, este actualizată după construire.
  • reducere : o modalitate sigură de a uni lucrarea de la toate firele după construcție.
Clauze de sincronizare
  • critic : blocul de cod inclus va fi executat de un singur fir la un moment dat și nu executat simultan de mai multe fire. Este adesea folosit pentru a proteja datele partajate de condițiile cursei .
  • atomic : actualizarea memoriei (scriere, sau citire-modificare-scriere) din următoarea instrucțiune va fi efectuată atomic. Nu face ca întreaga afirmație să fie atomică; numai actualizarea memoriei este atomică. Un compilator poate utiliza instrucțiuni hardware speciale pentru o performanță mai bună decât atunci când se utilizează critice .
  • ordonat : blocul structurat este executat în ordinea în care iterațiile ar fi executate într-o buclă secvențială
  • barieră : fiecare fir așteaptă până când toate celelalte fire ale unei echipe au ajuns la acest punct. O construcție de partajare a lucrului are o sincronizare implicită a barierei la sfârșit.
  • nowait : specifică faptul că firele care finalizează lucrările atribuite pot continua fără a aștepta finalizarea tuturor firelor din echipă. În absența acestei clauze, firele întâmpină o sincronizare de barieră la sfârșitul construcției de partajare a lucrărilor.
Clauze de programare
  • calendar (tip, bucată) : Acest lucru este util dacă construcția de partajare a lucrărilor este o buclă sau o buclă. Iterațiile din construcția de partajare a lucrărilor sunt atribuite thread-urilor în conformitate cu metoda de planificare definită de această clauză. Cele trei tipuri de planificare sunt:
  1. static : Aici, toate firele sunt alocate iterații înainte de a executa iterațiile buclei. Iterațiile sunt împărțite în mod egal în fire în mod implicit. Cu toate acestea, specificarea unui număr întreg pentru parametrul bloc va aloca numărul blocului de iterații contigue unui anumit fir.
  2. dinamic : Aici, unele dintre iterații sunt alocate unui număr mai mic de fire. Odată ce un anumit fir își termină iterația alocată, se întoarce pentru a obține altul din iterațiile rămase. Parametrul fragment definește numărul de iterații contigue care sunt alocate unui fir la un moment dat.
  3. ghidat : o bucată mare de iterații adiacente sunt alocate fiecărui fir dinamic (ca mai sus). Dimensiunea blocului scade exponențial cu fiecare alocare succesivă la o dimensiune minimă specificată în blocul de parametri
DACĂ controlul
  • dacă : Acest lucru va face ca firele să paralelizeze sarcina numai dacă este îndeplinită o condiție. În caz contrar, blocul de cod se execută în serie.
Inițializare
  • firstprivate : datele sunt private pentru fiecare fir, dar inițializate folosind valoarea variabilei folosind același nume din firul principal.
  • lastprivate : datele sunt private pentru fiecare fir. Valoarea acestor date private va fi copiată într-o variabilă globală folosind același nume în afara regiunii paralele dacă iterația curentă este ultima iterație din bucla paralelizată. O variabilă poate fi atât firstprivate, cât și lastprivate .
  • threadprivate : datele sunt date globale, dar sunt private în fiecare regiune paralelă în timpul rulării. Diferența dintre threadprivate și privat este domeniul global asociat cu threadprivate și valoarea păstrată în regiuni paralele.
Copierea datelor
  • copyin : similar cu firstprivate pentru variabilele private , variabilele threadprivate nu sunt inițializate, cu excepția cazului în care se utilizează copyin pentru a transmite valoarea din variabilele globale corespunzătoare. Nu este necesară nicio copiere, deoarece valoarea unei variabile threadprivate este menținută pe tot parcursul executării întregului program.
  • copyprivate : utilizat cu single pentru a susține copierea valorilor de date de la obiecte private pe un fir ( firul unic ) la obiectele corespunzătoare din alte fire din echipă.
Reducere
  • reducere (operator | intrinsec: listă) : variabila are o copie locală în fiecare fir, dar valorile copiilor locale vor fi rezumate (reduse) într-o variabilă partajată globală. Acest lucru este foarte util dacă o anumită operație (specificată în operator pentru această clauză specială) pe o variabilă rulează iterativ, astfel încât valoarea sa la o anumită iterație depinde de valoarea sa la o iterație anterioară. Pașii care duc la creșterea operațională sunt paraleli, dar firele actualizează variabila globală într-un mod sigur. Acest lucru ar fi necesar în paralelizarea integrării numerice a funcțiilor și a ecuațiilor diferențiale , ca un exemplu comun.
Alții
  • flush : Valoarea acestei variabile este restabilită din registru în memorie pentru utilizarea acestei valori în afara unei părți paralele
  • master : Executat numai de thread-ul master (firul care a forțat toate celelalte în timpul executării directivei OpenMP). Nicio barieră implicită; alți membri ai echipei (fire) care nu sunt obligați să ajungă.

Rutine de runtime la nivel de utilizator

Folosit pentru a modifica / verifica numărul de fire, pentru a detecta dacă contextul de execuție se află într-o regiune paralelă, câte procesoare din sistemul curent, blocări setate / dezactivate, funcții de sincronizare etc.

Variabile de mediu

O metodă de modificare a caracteristicilor de execuție ale aplicațiilor OpenMP. Folosit pentru a controla planificarea iterațiilor buclei, numărul implicit de fire, etc. De exemplu, OMP_NUM_THREADS este utilizat pentru a specifica numărul de fire pentru o aplicație.

Implementări

OpenMP a fost implementat în multe compilatoare comerciale. De exemplu, Visual C ++ 2005, 2008, 2010, 2012 și 2013 îl acceptă (OpenMP 2.0, în edițiile Professional, Team System, Premium și Ultimate), precum și Intel Parallel Studio pentru diverse procesoare. Compilatoarele și instrumentele Oracle Solaris Studio acceptă cele mai recente specificații OpenMP cu îmbunătățiri ale productivității pentru platformele Solaris (UltraSPARC și x86 / x64) și Linux. Compilatoarele Fortran, C și C ++ de la The Portland Group acceptă, de asemenea, OpenMP 2.5. GCC a acceptat și OpenMP de la versiunea 4.2.

Compilatoare cu implementarea OpenMP 3.0:

  • CCG 4.3.1
  • Compilator Mercurium
  • Compilatoare Intel Fortran și C / C ++ versiunile 11.0 și 11.1, Intel C / C ++ și Fortran Composer XE 2011 și Intel Parallel Studio.
  • Compilator IBM XL
  • Actualizarea 1 Sun Studio 12 are o implementare completă a OpenMP 3.0
  • Calcul multi-procesor ( "MPC" .)

Mai multe compilatoare acceptă OpenMP 3.1:

  • CCG 4.7
  • Compilatoare Intel Fortran și C / C ++ 12.1
  • Compilatoare IBM XL C / C ++ pentru AIX și Linux, compilatoare V13.1 și IBM XL Fortran pentru AIX și Linux, V14.1
  • LLVM / Clang 3.7
  • Absoft Fortran Compilers v. 19 pentru Windows, Mac OS X și Linux

Compilatoare care acceptă OpenMP 4.0:

  • GCC 4.9.0 pentru C / C ++, GCC 4.9.1 pentru Fortran
  • Compilatoare Intel Fortran și C / C ++ 15.0
  • IBM XL C / C ++ pentru Linux, V13.1 (parțial) și XL Fortran pentru Linux, V15.1 (parțial)
  • LLVM / Clang 3.7 (parțial)

Mai multe compilatoare care acceptă OpenMP 4.5:

  • GCC 6 pentru C / C ++
  • Compilatoare Intel Fortran și C / C ++ 17.0, 18.0, 19.0
  • LLVM / Clang 12

Suport parțial pentru OpenMP 5.0:

  • GCC 9 pentru C / C ++
  • Compilatoare Intel Fortran și C / C ++ 19.1
  • LLVM / Clang 12

Compilatoare auto-paralelizate care generează cod sursă adnotat cu directive OpenMP:

Mai mulți profileri și depanatori acceptă în mod expres OpenMP:

Argumente pro şi contra

Pro:

  • Cod portabil multithreading (în C / C ++ și alte limbi, de obicei trebuie să apelați primitive specifice platformei pentru a obține multithreading).
  • Simplu: nu trebuie să vă ocupați de transmiterea mesajelor, așa cum face MPI .
  • Aspectarea și descompunerea datelor este gestionată automat de directive.
  • Scalabilitate comparabilă cu MPI pe sistemele de memorie partajată.
  • Paralelism incremental: poate funcționa simultan pe o parte a programului, nu este necesară nicio schimbare dramatică a codului.
  • Cod unificat atât pentru aplicații seriale, cât și pentru aplicații paralele: constructele OpenMP sunt tratate ca comentarii atunci când sunt utilizate compilatoare secvențiale.
  • Instrucțiunile de cod originale (serial) nu trebuie, în general, modificate atunci când sunt paralelizate cu OpenMP. Acest lucru reduce șansa introducerii accidentale a erorilor.
  • Atât granulat grosier și granulat fin paralelism sunt posibile.
  • În aplicațiile neregulate multi-fizica , care fac să nu adere numai la SPMD modul de calcul, astfel cum întâlnite în sistemele de fluid-particule strâns cuplate, flexibilitatea OpenMP poate avea un avantaj de performanță mare peste MPI .
  • Poate fi folosit pe diverse acceleratoare, cum ar fi GPGPU și FPGA .

Contra:

  • Riscul introducerii erorilor de sincronizare dificil de depanat și a condițiilor de cursă .
  • Începând cu 2017 rulează eficient doar pe platforme multiprocesor cu memorie partajată (a se vedea totuși Cluster OpenMP Intel și alte platforme de memorie partajată distribuită ).
  • Necesită un compilator care acceptă OpenMP.
  • Scalabilitatea este limitată de arhitectura memoriei.
  • Nu există suport pentru comparație și swap .
  • Tratarea fiabilă a erorilor lipsește.
  • Lipseste mecanisme cu granulație fină pentru a controla maparea procesorului de fire.
  • Șanse mari de a scrie accidental cod de partajare fals .

Așteptări de performanță

S-ar putea aștepta să obțineți o viteză de N ori atunci când rulați un program paralelizat utilizând OpenMP pe o platformă de procesor N. Cu toate acestea, acest lucru apare rar din aceste motive:

  • Când există o dependență, un proces trebuie să aștepte până când datele de care depinde sunt calculate.
  • Când mai multe procese partajează o resursă de probă non-paralelă (cum ar fi un fișier în care să scrie), cererile lor sunt executate secvențial. Prin urmare, fiecare fir trebuie să aștepte până când celălalt fir eliberează resursa.
  • O mare parte a programului nu poate fi paralelizată de OpenMP, ceea ce înseamnă că limita teoretică superioară a accelerării este limitată conform legii lui Amdahl .
  • N procesoare într-o multiprocesare simetrică (SMP) pot avea de N ori puterea de calcul, dar lățimea de bandă a memoriei nu crește de obicei de N ori. Destul de des, calea de memorie originală este partajată de mai multe procesoare și degradarea performanței poate fi observată atunci când concurează pentru lățimea de bandă a memoriei partajate.
  • Multe alte probleme comune care afectează accelerarea finală în calculul paralel se aplică și OpenMP, cum ar fi echilibrarea sarcinii și sincronizarea overhead.
  • Este posibil ca optimizarea compilatorului să nu fie la fel de eficientă atunci când invocați OpenMP. Acest lucru poate duce în mod obișnuit la un program OpenMP cu un singur fir care rulează mai lent decât același cod compilat fără un flag OpenMP (care va fi complet serial).

Afinitatea firului

Unii furnizori recomandă setarea afinității procesorului pe firele OpenMP pentru a le asocia cu anumite nuclee de procesor. Acest lucru minimizează migrarea firelor și costul de comutare a contextului între nuclee. De asemenea, îmbunătățește localitatea datelor și reduce traficul de coerență a cache-ului între nuclee (sau procesoare).

Repere

O varietate de repere a fost dezvoltată pentru a demonstra utilizarea OpenMP, a testa performanța acestuia și a evalua corectitudinea.

Exemple simple

Reperele de performanță includ:

Punctele de referință privind corectitudinea includ:

Vezi si

Referințe

Lecturi suplimentare

linkuri externe