La differenza è che nel primo blocco si è non davvero la creazione di qualsiasi attività in quanto il blocco di per sé non è nidificato (né sintatticamente né lessicale) all'interno di una attiva regione parallela. Nel secondo blocco il costrutto task
è nidificato sinteticamente all'interno della regione parallel
e farebbe la coda delle attività esplicite se la regione è attiva al momento dell'esecuzione (una regione parallela attiva è quella che viene eseguita con un gruppo di più thread). Il nesting lessicale è meno ovvio. Osservare il seguente esempio:
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;
}
La prima chiamata a foo()
accade all'esterno di qualsiasi regioni parallele. Quindi la direttiva task
non esegue (quasi) nulla e tutte le chiamate a bar()
avvengono in serie. La seconda chiamata a foo()
proviene dall'interno della regione parallela e quindi verranno generate nuove attività all'interno di foo()
. La regione parallel
è attiva poiché il numero di thread è stato corretto su 4
dalla clausola num_threads(4)
.
Questo diverso comportamento delle direttive OpenMP è una funzionalità di progettazione. L'idea principale è quella di essere in grado di scrivere codice che possa essere eseguito sia in serie che in parallelo.
Ancora la presenza del costrutto task
in foo()
esegue la trasformazione del codice, ad es. foo()
si trasforma in qualcosa di simile:
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);
}
Qui OMP_make_task()
è una funzione ipotetica (non disponibili al pubblico) dalla libreria di supporto OpenMP che mette in coda una chiamata alla funzione, fornito come primo argomento. Se OMP_make_task()
rileva che funziona al di fuori di un'area parallela attiva, chiamerebbe semplicemente foo_omp_fn_1()
. Ciò aggiunge un sovraccarico alla chiamata a bar()
nel caso seriale. Invece di main -> foo -> bar
, la chiamata va come main -> foo -> OMP_make_task -> foo_omp_fn_1 -> bar
. L'implicazione di questo è l'esecuzione del codice seriale più lenta.
questo è ancora più evidente illustrata con la direttiva worksharing:
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;
}
La prima chiamata a foo()
correrebbe il circuito in serie. La seconda chiamata distribuiva le 12 iterazioni tra i 4 thread, cioè ogni thread eseguiva solo 3 iter. Ancora una volta, per ottenere questo risultato viene utilizzata una magia di trasformazione del codice e il ciclo seriale verrebbe eseguito più lentamente rispetto a quando non era presente #pragma omp for
in foo()
.
La lezione qui è di non aggiungere mai costrutti OpenMP dove non sono realmente necessari.
+1 Risposta piacevole. – dreamcrash
Sembra che ho commesso un errore nell'utilizzo del compito.Questo problema è sorto perché ho visto un codice di attraversare ricorsivamente l'albero con solo "task" come ho aggiunto nella domanda. Immagino che ci debbano essere "paralleli" e "singoli" che racchiudano la funzione trasversale dove viene chiamata. Mille grazie per la tua risposta sincera. –
@AnnieKim, sì, la funzione 'traverse()' come mostrato nella domanda attraverserebbe l'albero in parallelo se chiamata dall'interno di una regione 'parallela 'attiva e in serie altrimenti. Questa è la bellezza di OpenMP :) (nonostante l'overhead aggiunto) –