OpenMP (ang. Open Multi-Processing) – wieloplatformowy interfejs programowania aplikacji (API) umożliwiający tworzenie programów komputerowych dla systemów wieloprocesorowych z pamięcią dzieloną. Może być wykorzystywany w językach programowania C, C++ i Fortran na wielu architekturach, m.in. Unix i Microsoft Windows. Składa się ze zbioru dyrektyw kompilatora, bibliotek oraz zmiennych środowiskowych mających wpływ na sposób wykonywania się programu.
Dzięki temu, że standard OpenMP został uzgodniony przez głównych producentów sprzętu i oprogramowania komputerowego, charakteryzuje się on przenośnością, skalowalnością, elastycznością i prostotą użycia. Dlatego może być stosowany do tworzenia aplikacji równoległych dla różnych platform, od komputerów klasy PC po superkomputery.
OpenMP można stosować do tworzenia aplikacji równoległych działających na wieloprocesorowych węzłach klastrów komputerowych. W tym przypadku stosuje się rozwiązanie hybrydowe, w którym programy są uruchamiane na klastrach komputerowych pod kontrolą alternatywnego interfejsu MPI, natomiast do zrównoleglenia pracy węzłów klastrów wykorzystuje się OpenMP. Alternatywny sposób polegał na zastosowaniu specjalnych rozszerzeń OpenMP dla systemów pozbawionych pamięci współdzielonej (np. Cluster OpenMP Intela).
Wprowadzenie thumb|500px|Zasada implementacji współbieżności w OpenMP: główny wątek co pewien czas rozgałęzia się na pewną liczbę wątków potomnych współbieżnie wykonujących określone fragmenty kodu. Za każdym razem liczba wątków potomnych może być inna, a obciążenia wątków przy realizacji konkretnych zadań mogą być zróżnicowane. Celem OpenMP jest implementacja wielowątkowości, czyli metody zrównoleglania programów komputerowych, w której główny „wątek programu” (czyli ciąg następujących po sobie instrukcji) „rozgałęzia” się na kilka „wątków potomnych”, które wspólnie wykonują określone zadanie. Wątki pracują współbieżnie i mogą zostać przydzielone przez środowisko uruchomieniowe różnym procesorom. Fragment kodu, który ma być wykonywany równolegle, jest w kodzie źródłowym oznaczany odpowiednią dyrektywą preprocesora. Tuż przed wykonaniem tak zaznaczonego kodu główny wątek rozgałęzia się na określoną liczbę nowych wątków. Każdy wątek posiada unikatowy identyfikator (ID), którego wartość można odczytać funkcją omp_get_thread_num() (w C/C++) lub OMP_GET_THREAD_NUM() (w Fortranie). Identyfikator wątku jest liczbą całkowitą, przy czym identyfikator wątku głównego równy jest 0. Po zakończeniu przetwarzania zrównoleglonego kodu wątki „włączają się” z powrotem do wątku głównego, który samotnie kontynuuje działanie programu i w innym miejscu może ponownie rozdzielić się na nowe wątki.
Domyślnie wszystkie wątki przetwarzają zrównolegloną część kodu niezależnie od siebie. Do podziału zadania pomiędzy wątki służą specjalne „konstrukcje podziału zadania”. Posługując się nimi można w OpenMP osiągnąć zarówno zrównoleglenie na poziomie zadań (ang. task parallelism), jak i na poziomie danych (ang. data parallelism).
Za przydział wątków do poszczególnych procesorów odpowiada środowisko uruchomieniowe, które stosuje algorytm uwzględniający m.in. aktualne obciążenie poszczególnych procesorów oraz całej maszyny. Liczba nowych wątków może być określona tuż przed uruchomieniem programu za pośrednictwem zmiennych środowiskowych lub w kodzie źródłowym za pomocą specjalnych funkcji. Deklaracje funkcji biblioteki OpenMP dla języków C i C++ znajdują się w pliku nagłówkowym "omp.h").
Historia Specyfikacje kolejnych wersji standardu OpenMP publikowane są przez OpenMP Architecture Review Board (ARB). Pierwsza z nich, tzw. OpenMP for Fortran v. 1.0, została opublikowana w październiku 1997 roku i dotyczyła wyłącznie języka Fortran. Miesiąc później opublikowano wersję Fortran 1.1. Pierwszy standard dla języków C i C++ (OpenMP for C/C++ 1.0) został opublikowana w październiku następnego roku. W 2000 roku opublikowano wersję 2.0 dla języka Fortran, a w 2002 wersję 2.0 dla języków C/C++. Pierwszą wspólną specyfikacją dla języków C, C++ i Fortran jest opublikowana w 2005 wersja 2.5.
Najnowszą wersją standardu jest OpenMP 3.0 z maja 2008 roku. Do nowych cech tego standardu należy konstrukcja task. Szczegółowy wykaz nowych cech standardu 3.0 znajduje się w Dodatku F specyfikacji OpenMP 3.0.
Kompilacja i uruchomienie programu OpenMP Przykład kompilacji dla kompilatora Intel C++:
$ icc -openmp program.cpp -o program
Przykład kompilacji dla kompilatora GCC:
$ g++ -fopenmp program.cpp -o program
Uruchomienie programu dla czterech wątków:
$ env OMP_NUM_THREADS=4 ./program
Podstawowe elementy center|500px|thumb|Diagram konstrukcji OpenMP
Podstawowymi elementami OpenMP są konstrukcje służące do tworzenia wątków, rozdzielania (współdzielenia) pracy, zarządzania środowiskiem danych, synchronizacji wątków, procedury czasu wykonania (poziomu użytkownika) i zmienne środowiskowe.
Dyrektywa kompilatora C/C++ pragma (ang. pragmatic information), przy użyciu OpenMP jest zapisywana w kodzie następująco: #pragma omp <reszta "pragmy">
Specyficzne dla OpenMP "pragmy":
Tworzenie wątku omp parallel. Jest używana do przydzielenia dodatkowych wątków do równoległego wykonania pracy zawartej w konstrukcji. Oryginalny proces zostanie oznaczony jako główny wątek z ID równym 0.
Przykład: Wyświetl "Hello, world" przy użyciu wielu wątków.
int main(int argc, char* argv[]) { #pragma omp parallel printf("Hello, world!\n"); return 0;
Konstrukcje związane ze współdzieleniem pracy Konstrukcje te są używane do ustalenia, jak przyporządkować niezależną pracę do jednego lub wszystkich wątków. omp for lub omp do – używane do rozdzielania iteracji pętli pomiędzy wątki, zwane także konstrukcjami pętli. sections – przypisanie następujących po sobie, ale niezależnych bloków kodu do różnych wątków single – wyszczególnienie bloku kodu, który będzie przetwarzany przez tylko jeden wątek, z barierą na końcu master – podobne do single, przy czym kod jest przetwarzany tylko przez wątek główny, bez bariery na końcu
Przykład: równoległa inicjalizacja wartości dużej tablicy, każdy wątek wykonuje część pracy:
int main(int argc, char **argv) { const int N = 100000; int i, a[N];
#pragma omp parallel for for (i = 0; i < N; i++) a[i] = 2 * i;
return 0;
Klauzule OpenMP Ponieważ OpenMP jest modelem programowania ze współdzieloną pamięcią, większość zmiennych w OpenMP jest domyślnie widoczna dla wszystkich wątków. Ponieważ czasami prywatne zmienne są niezbędne, aby zapobiec błędnemu stanowi przy przesyłaniu wartości pomiędzy sekwencyjną a równoległą częścią programu (blokiem kodu wykonywanym równolegle), środowisko zarządzania danymi jest realizowane za pomocą klauzul atrybutów współdzielenia danych poprzez dołączenie ich do dyrektyw OpenMP.
Różne typy warunków:
Klauzule atrybutów współdzielenia danych shared – dane wewnątrz równolegle przetwarzanego regionu są współdzielone, co oznacza, że są widoczne i dostępne dla wszystkich wątków jednocześnie. Domyślnie wszystkie zmienne w regionie współdzielonej pracy są shared z wyjątkiem licznika iteracji pętli. private – dane wewnątrz równolegle przetwarzanego regionu są prywatne dla każdego wątku, co oznacza, że każdy wątek posiada lokalną kopię i używa jej jako zmienną tymczasową. Prywatne zmienne nie są inicjalizowane, ich wartość nie jest udostępniana do użycia poza regionem przetwarzanym równolegle. Domyślnie liczniki iteracji pętli są prywatne. default – pozwala programiście ustawiać domyślny zasięg zmiennych wewnątrz równolegle przetwarzanego regionu, poprzez użycie shared, private lub none. Opcja none zmusza programistę do deklarowania każdej zmiennej w równolegle przetwarzanym regionie przy użyciu klauzul atrybutów współdzielenia danych.
Klauzule synchronizacji critical section – zawarty blok kodu będzie przetwarzany przez tylko jeden wątek w danym czasie, a nie jednocześnie przez kilka. Jest często używany do ochrony współdzielonych danych przed błędnym stanem atomic – podobne do critical section, ale informuje kompilator aby zostały użyte specjalne instrukcje sprzętowe dla zapewnienia lepszej wydajności. Kompilatory mogą zignorować tę sugestię od użytkownika i użyć critical section. ordered – zawarty blok jest przetwarzany w porządku w którym iteracje są uruchamiane w sekwencyjnej pętli barrier – każdy wątek czeka, zanim wszystkie inne wątki osiągną ten punkt. Konstrukcje współdzielące prace posiadają ukrytą synchronizację barier na końcu. nowait – ustala, że wątek kompletujący przyporządkowaną mu pracę, może pracować bez czekania na inne wątki przed zakończeniem. W przypadku braku tej klauzuli, wątki przeprowadzają synchronizację bariery na końcu bloku pracy współdzielonej.
Klauzule planowania schedule(type, chunk) – Jest to użyteczne jeśli konstrukcja współdzieląca prace jest pętlą do lub for. Określa sposób podziału pętli na części wykonywane równolegle. Iteracja(iteracje) w konstrukcji współdzielącej pracę jest przyporządkowana do wątku bazując na metodzie planującej zdefiniowanej przez tę klauzulę. Parametr „type” określający sposób rozdziału iteracji (sposób szeregowania) jest obowiązkowy (sposoby planowania są opisanie poniżej). Drugi parametr „chunk” jest opcjonalny. Określa on wielkość równolegle przetwarzanej części. Musi być liczbą całkowitą. Poszczególne wątki w blokach są rozmiaru „chunk” do których przydzielane są iteracje. Typy planowania to:
static – Tutaj, każdy z wątków posiada przypisane iteracje przed uruchomieniem iteracji pętli. Domyślnie, iteracje są dzielone po równo między wątki. Określenie całkowitej wartości dla parametru „chunk”, spowoduje przydzielenie tej ilości następujących po sobie iteracji do konkretnego wątku. Jeżeli programista pominie parametr „chunk”, zbiór iteracji zostanie podzielony na podzbiory o wielkości (w przybliżeniu) równej liczba_iteracji/liczba_wątków, a każdy wątek otrzyma przynajmniej jeden podzbiór. dynamic – Tutaj, pewna ilość iteracji jest przypisywana do mniejszej ilości wątków. Gdy konkretny wątek skończy swoją iterację, pobiera następną z pozostałych wolnych iteracji. Ostatni podzbiór może mieć mniejszą wielkość niż ta określona przez parametr „chunk”. Parametr „chunk” definiuje ilość iteracji, które są przyporządkowywane do wątku za każdym razem. Gdy „chunk” nie jest określony, wówczas podzbiory są jednoelementowe. guided – Duży fragment następujących po sobie iteracji jest przypisywany do każdego wątku dynamicznie (jak powyżej). Rozmiar fragmentu zmniejsza się wykładniczo, wraz z każdą pomyślną alokacją, do minimalnego rozmiaru zdefiniowanego w parametrze „chunk”. Jeśli programista ustawi parametr „chunk” na 1, to rozmiar każdego zbioru jest opisany algorytmem: liczba_nieprzydzielonych_iteracji/liczba_wątków. Wartość ta zmierza do 1. Adekwatnie jeśli „chunk” ustalony zostaje na m gdzie m>1, to rozmiar porcji ustalany jest podobnie jak wyżej, lecz nie może on stać się mniejszy od m. Wyjątkiem jest jedynie ostatnia porcja. Może ona być mniejsza od zadanej wielkości. Domyślny „chunk” (gdy nie jest ustalony) jest równy 1. „auto” – System samodzielnie określa podział zrównoleglenia zadania. „runtime” – Ta opcja zezwala na określanie na wykorzystywanie powyższych możliwości w trakcie działania programu. Przekazywanie jest powiązane ze zmienną środowiskową OMP_SCHEDULE:
$ export OMP_SCHEDULE="dynamic" $ export OMP_SCHEDULE="guided,4"
Kontrola IF if – Powoduje, że wątki zrównoleglają zadanie tylko wtedy gdy warunek jest spełniony. W innym wypadku blok kodu jest przetwarzany szeregowo.
Inicjalizacja firstprivate – dane prywatne dla każdego wątku, inicjalizowane przy użyciu wartości zmiennych o nazwach takich jak w głównym wątku. lastprivate – dane prywatne dla każdego wątku. Wartości są kopiowane na zewnątrz zrównoleglonego rejonu, do zmiennych globalnych o takiej samej nazwie, jeśli bieżąca iteracja jest ostatnią iteracją zrównoleglonej pętli. Zmienna może być zarówno firstprivate, jak i lastprivate.