Разница заключается в том, что в первом блоке вы не действительно создает каких-либо задач, так как сам блок не вложен (ни синтаксически, ни лексически) внутри активной параллельная область. Во втором блоке конструкция task
синтаксически вложена внутри области parallel
и будет ставить в очередь явные задачи, если регион будет активен во время выполнения (активная параллельная область - это область, которая выполняется с командой из более чем одного потока). Лексическое гнездование менее очевидно. Обратите внимание на следующий пример:
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;
}
Первый вызов foo()
происходит вне каких-либо параллельных областей. Следовательно, директива task
делает (почти) ничего, и все звонки в bar()
происходят серийно. Второй вызов foo()
происходит изнутри параллельной области, и, следовательно, новые задачи будут генерироваться внутри foo()
. Область parallel
активна, так как число потоков было зафиксировано на 4
по статье num_threads(4)
.
Это различное поведение директив OpenMP является конструктивной особенностью. Основная идея заключается в том, чтобы иметь возможность писать код, который может выполняться как последовательный, так и параллельный.
Тем не менее наличие конструкции task
в foo()
выполняет некоторые преобразования кода, например. foo()
превращаются в нечто вроде:
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);
}
Здесь OMP_make_task()
гипотетическая (не является общедоступной) функция из библиотеки поддержки OpenMP, что в очереди вызова функции, поставляемый в качестве первого аргумента. Если OMP_make_task()
обнаруживает, что он работает вне активной параллельной области, вместо этого он просто вызывает foo_omp_fn_1()
. Это добавляет некоторые накладные расходы на вызов bar()
в серийном корпусе. Вместо main -> foo -> bar
звонок идет как main -> foo -> OMP_make_task -> foo_omp_fn_1 -> bar
. Следствием этого является более медленное выполнение серийного кода.
Это еще более очевидно, показано с директивой распараллеливание:
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;
}
Первый вызов foo()
будет запускать цикл в последовательный. Второй вызов распределил бы 12 итераций среди 4 потоков, т. Е. Каждый поток выполнил бы только 3 итератора. Еще раз для этого используется магия преобразования кода, и последовательный цикл будет работать медленнее, чем если бы #pragma omp for
присутствовал в foo()
.
Урок здесь заключается в том, чтобы никогда не добавлять конструкции OpenMP, где они действительно не нужны.
+1 Хороший ответ. – dreamcrash
Кажется, я допустил ошибку при использовании задачи.Эта проблема возникла из-за того, что я видел код рекурсивного пересечения дерева только с «заданием», как я добавил в вопросе. Я предполагаю, что должны быть «параллельные» и «одиночные», охватывающие функцию перемещения, где она вызывается. Огромное спасибо вашему искреннему ответу. –
@AnnieKim, да, функция 'traverse()', как показано в вашем вопросе, будет пересекать дерево параллельно, если вызывается из активной области 'parallel' и в противном случае в противном случае. Это красота OpenMP :) (несмотря на дополнительные накладные расходы) –