Kurs jest częścią programu Xtreme Computing.

Autorem kursu jest Paweł Przybyłowicz - asystent na Wydziale Matematyki Stosowanej AGH.

Krok 2 - zrównoleglanie pętli, opcje default i schedule.

W Lekcji 2 kontynuujemy temat zrównoleglania pętli za pomocą standardu OpenMP. Omówimy teraz dyrektywy default i schedule.

Dyrektywa default

Dyrektywa default nadaje zmiennym, wykorzystywanym w pętli, domyślne atrybuty określające czy są typu private czy też typu nieokreślonego. W języku C/C++ mamy dwa dostępne sposoby wykorzystania tego polecenia. Możemy napisać: Pisząc:
  1. default(shared) żądamy, aby wszystkie zmienne były wspólne. Jeśli jakieś zmienne mają być prywatne musimy to zakomunikować dopisując dyrektywę private. W programie z Lekcji 1 może to wyglądać następująco:
    #pragma omp parallel for \ default(shared) private(i, j)
    Takie użycie dyrektywy default(shared) powoduje, że domyślnie wszystkie zmienne w pętli są wspólne. W naszym przypadku są to: A, u, v, rows, columns, i oraz j. Ponieważ zmienne i i j muszą być prywatne, dlatego dopisaliśmy private(i, j).
  2. default(none) deklarujemy, że domyślnie żadna zmienna nie jest ani prywatna ani wspólna. Programista sam musi określić typ każdej zmiennej. Takie podejście zastosowaliśmy w programie z Lekcji 1. W praktyce rekomenduje się użycie tej dyrektywy, gdyż zmusza to programistę do jawnego określenia typu każdej zmiennej, co prowadzi do mniejszej liczby pomyłek. W naszym programie użycie tej dyrektywy będzie wyglądać następująco:
    #pragma omp parallel for \ default(none) shared(A, u, v, rows, columns) private(i, j)

Dyrektywa schedule

Kolejną dyrektywą jaką omówimy będzie opcja schedule. Jej wykorzystanie ogranicza się jedynie do pętli for. Użyta w innym kontekście będzie przyczyną błędu zasygnalizowanego przez kompilator. Dyrektywą schedule możemy w pewnym stopniu kontrolować sposób rozdzielania iteracji pomiędzy dostępne wątki. Składnia tej dyrektywy następująca:
schedule(sposób_rozdziału_iteracji, [chunk])
W programie wymaga ona obowiązkowo podania pierwszego parametru, drugi parametr - chunk - jest opcjonalny. Zmienna chunk określa liczność każdego podzbioru iteracji, na które dzielimy zbiór wszystkich dostępnych iteracji. Podzbiory te będą w określony sposób przypisane dostępnym wątkom. Ich liczność nie musi mieć wartości stałej i może się zmieniać w trakcie wykonywania pętli. W zależności od pierwszego parametru dyrektywy schedule mamy następujące możliwości:
  1. static, wówczas iteracje rozdzielane są na zbiory o rozmiarze chunk. Następnie są one kolejno przydzielane dostępnym wątkom. Jeśli np.: program ma do wykonania 10 iteracji, a mamy dwa wątki, wówczas najlepiej przyjąć za chunk wartość równą 5. Oznacza to, że mamy dwa zbiory po 5 iteracji, które zostaną przypisane oddzielnym wątkom. Jeśli programista nie określi wielkości zmiennej chunk, wówczas zbiór iteracji jest dzielony na podzbiory, których wielkość wynosi w przybliżeniu liczba_iteracji / liczba_wątków. Każdemu wątkowi zostanie przypisany co najwyżej jeden taki podzbiór.
  2. dynamic, wtedy każdemu wątkowi przypisywana jest liczba iteracji określona przez parametr chunk. Po wykonaniu obliczeń wątki otrzymują kolejną porcję iteracji do wykonania. Ostatni przydzielony podzbiór iteracji może mieć wielkość mniejszą niż ta, która jest określona przez chunk. Jeśli programista nie określił wielkości chunk, to podzbiory są jedno-elementowe.
  3. guided, wówczas iteracje rozdzielane są pomiędzy wątki podobnie jak przy opcji dynamic. Różnica polega na tym, że w tym przypadku rozmiar przypisywanych podzbiorów iteracji zmniejsza się w czasie. Jeśli za chunk przyjmiemy w programie wartość 1, to rozmiar każdego przypisywanego zbioru iteracji jest proporcjonalny do wielkości liczba_nie_przydzielonych_iteracji / liczba_wątków. W trakcie wykonywania pętli wielkość ta zmierza do 1. Jeśli przyjmiemy chunk=m (m>1), to rozmiar każdej porcji iteracji jest ustalany jak wyżej, z tą różnicą, że rozmiar ten nie może być mniejszy od m. Wyjątek stanowi ostatni przydzielony zbiór iteracji, którego wielkość może być mniejsza niż m. Gdy parametr chunk nie jest jawnie określony, to jego domyślna wartość wynosi 1.
  4. runtime, wtedy jedna z powyższych opcji oraz wartość chunk jest ustalana w czasie działania programu, na podstawie zmiennej środowiskowej OMP_SCHEDULE.
W praktyce polecamy najpierw użycie opcji static, bez parametru chunk. Następnie można wypróbować pozostałe opcje, jednocześnie próbując dobrać do zadania optymalną wielkość chunk.

Poniższy program ilustruje, jak opcja schedule działa w praktyce.

Program 02/01 - step_0201.c

#include <stdio.h> #include <stdlib.h> #include <omp.h> int main(int argc, char *argv[]) { int i, n, id, chunk_size; n = 9; chunk_size = 1; #pragma omp parallel default(none) private(i, id) shared(n, chunk_size) { #pragma omp single { printf("Program jest wykonywany na %d watkach.\n", omp_get_num_threads()); } #pragma omp for schedule(static, chunk_size) for (i = 0; i < n; i++) { id = omp_get_thread_num(); printf("Iteracja %d wykonana przez watek nr. %d.\n", i, id); } } return 0; }
Funkcje biblioteczne omp_get_num_threads() oraz omp_get_thread_num() zwracają odpowiednio liczbę wątków wykorzystanych w sekcji parallel oraz numer wątku, który wykonuje dane zadanie - w tym przypadku numer wątku, który wykonuje określoną iterację. Ponadto w programie pojawiła się nowa dyrektywa OpenMP, tj. dyrektywa single. Sposób jej użycia jest bardzo prosty. Umieszczając ją w sekcji parallel nakazujemy programowi aby dany blok kodu był wykonany tylko przez jeden wątek. Należy zaznaczyć, że nie wybieramy, który wątek ma ten blok wykonywać. Pozostałe wątki czekają, dopóki ten jeden nie zakończy wykonywać zleconego mu zadania, a następnie przechodzą do dalszej części programu. Po uruchomieniu program najpierw podaje liczbę dostępnych wątków, a następnie wypisuje, która iteracja w pętli for jest wykonywana przez który wątek. Należy pamiętać, że wątki numerowane są od 0. W zależności od tego jaką opcje wybierzemy w dyrektywie schedule, zobaczymy określony przydział iteracji do wątków. Polecamy eksperymentować z programem (ćwiczenia 2 i 3).

Ćwiczenia:

  1. W programie z Lekcji 1 dodaj opcję schedule z różnymi parametrami. Jak wpływa to na czas wykonywania programu?
  2. Sprawdź jak powyższy program zachowuje się z opcjami innymi niż static. Wypróbuj różne wartości zmiennej n oraz chunk_size.
  3. Sprawdź co się stanie, gdy wykasujesz linię #pragma omp single z powyższego programu. Czy potrafisz wyjaśnić zaobserwowany efekt?

Kolejne lekcje tutorialu:

OpenMP.pl - polski portal użytkowników standardu OpenMP.