具有和不具有并行的OpenMP任务


Node *head = &node1;
while (head)
{
#pragma omp task 
cout<<head->value<<endl;
head = head->next;
}
#pragma omp parallel
{
#pragma omp single
{
Node *head = &node1;
while (head)
{
#pragma omp task 
cout<<head->value<<endl;
head = head->next;
}
}
}

在第一个块中,我只是创建了没有并行指令的任务,而在第二个块中我使用了并行指令和单指令,这是我在论文中看到的常见方式。我想知道它们之间有什么区别?顺便说一句,我知道这些指令的基本含义。

我评论中的代码:

void traverse(node *root)
{
if (root->left) 
{
#pragma omp task 
traverse(root->left);
}
if (root->right) 
{
#pragma omp task 
traverse(root->right);
}
process(root);
}

不同之处在于,在第一个块中,并不是真正创建任何任务,因为块本身没有嵌套在活动的并行区域中(无论是语法上还是词汇上)。在第二个块中,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()内部生成新任务。由于num_threads(4)子句将线程数固定为4,因此parallel区域处于活动状态。

OpenMP指令的这种不同行为是一种设计特性。其主要思想是能够编写既可以串行执行也可以并行执行的代码。

尽管如此,foo()task结构的存在进行了一些代码转换,例如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 -> OMP_make_task -> foo_omp_fn_1 -> bar类似,而不是main -> foo -> 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()的第一个调用将串行运行循环。第二个调用将在4个线程之间分配12次迭代,即每个线程只执行3次迭代。再次,使用一些代码转换魔法来实现这一点,并且串行循环的运行速度将比foo()中不存在#pragma omp for时慢。

这里的教训是永远不要在不需要的地方添加OpenMP构造。

最新更新