Różnica polega na tym, że w pierwszym bloku jesteś nie naprawdę tworzenie zadań, ponieważ sam blok nie jest zagnieżdżona (ani składniowo ani leksykalnie) wewnątrz aktywnym region równoległy. W drugim bloku konstrukcja task
jest syntaktycznie zagnieżdżona w regionie parallel
i może kolejkować jawne zadania, jeśli region stanie się aktywny w czasie wykonywania (aktywny region równoległy to taki, który wykonuje się w zespole złożonym z więcej niż jednego wątku). Zagnieżdżanie leksykalne jest mniej oczywiste. Należy zwrócić uwagę na poniższy przykład:
void foo(void)
{
int i;
for (i = 0; i < 10; i++)
#pragma omp task
bar();
}
int main(void)
{
foo();
#pragma omp parallel num_threads(4)
{
#pragma omp single
foo();
}
return 0;
}
Pierwsze wezwanie do foo()
stanie całkowicie poza równoległych obszarach. Stąd dyrektywa task
robi (prawie) nic i wszystkie połączenia z bar()
odbywają się seryjnie. Drugie wywołanie foo()
pochodzi z wnętrza równoległego regionu, a zatem nowe zadania zostaną wygenerowane wewnątrz foo()
. Region parallel
jest aktywny, ponieważ liczba wątków została ustalona na 4
przez klauzulę num_threads(4)
.
To inne zachowanie dyrektyw OpenMP to funkcja projektowania. Główną ideą jest możliwość napisania kodu, który mógłby wykonywać zarówno jako szeregowy, jak i równoległy.
Wciąż obecność konstruktu task
w foo()
powoduje pewną transformację kodu, np. foo()
przekształca się w coś w rodzaju:
void foo_omp_fn_1(void *omp_data)
{
bar();
}
void foo(void)
{
int i;
for (i = 0; i < 10; i++)
OMP_make_task(foo_omp_fn_1, NULL);
}
Tutaj OMP_make_task()
jest hipotetycznym (nie są publicznie dostępne) funkcja z biblioteki wsparcia OpenMP że kolejkuje wywołanie funkcji, dostarczony jako pierwszy argument. Jeśli OMP_make_task()
wykryje, że działa poza aktywnym regionem równoległym, zamiast tego po prostu zadzwoni pod numer foo_omp_fn_1()
. To dodaje trochę kosztów do połączenia z numerem bar()
w przypadku szeregowym. Zamiast main -> foo -> bar
, wywołanie wygląda następująco: main -> foo -> OMP_make_task -> foo_omp_fn_1 -> bar
. Implikacją tego jest wolniejsze wykonanie kodu szeregowego.
ta jest jeszcze bardziej oczywisty ilustrowane dyrektywy podziału pracy:
void foo(void)
{
int i;
#pragma omp for
for (i = 0; i < 12; i++)
bar();
}
int main(void)
{
foo();
#pragma omp parallel num_threads(4)
{
foo();
}
return 0;
}
Pierwsze wywołanie foo()
by uruchomić pętlę w serial. Drugie wywołanie rozprowadzałoby 12 iteracji między 4 wątkami, tj. Każdy wątek wykonywałby tylko 3 iteracje. Ponownie, do osiągnięcia tego celu użyta jest pewna magia transformacji kodu, a pętla szeregowa działałaby wolniej, niż gdyby #pragma omp for
nie było obecne w .
Lekcja tutaj polega na tym, aby nigdy nie dodawać konstrukcji OpenMP tam, gdzie nie są one naprawdę konieczne.
+1 Dobra odpowiedź. – dreamcrash
Wygląda na to, że popełniłem błąd w korzystaniu z zadania.Ten problem pojawił się, ponieważ widziałem kod rekurencyjnie przechodzący przez drzewo z "tylko zadaniem", jak dodałem w pytaniu. Sądzę, że powinien istnieć "równoległy" i "pojedynczy" obejmujący funkcję trawersu tam, gdzie się ją nazywa. Wielkie dzięki dla Twojej szczerej odpowiedzi. –
@AnnieKim, tak, funkcja 'traverse()', jak pokazano w twoim pytaniu, będzie poruszać się równolegle do drzewa, jeśli zostanie wywołana z aktywnego regionu 'równoległego' i szeregowo inaczej. To jest piękno OpenMP :) (pomimo dodanego narzutów) –