我是 Openmp 编程新手,我有一个关于递归任务并行性的问题
让我们考虑一下这个演示 C 代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <omp.h>
struct timeval t1, t2;
void recursive_task(int level)
{
//printf("%d\n", level);
if (level == 0){
usleep(1000);
return;
}
else
{
recursive_task(level-1);
#pragma omp task
{
recursive_task(level-1);
}
#pragma omp task
{
recursive_task(level-1);
}
#pragma omp taskwait
recursive_task(level-1);
}
}
int main()
{
double time;
gettimeofday(&t1, 0);
#pragma omp parallel
{
#pragma omp single
{
recursive_task(7);
}
}
gettimeofday(&t2, 0);
time = (double)((t2.tv_sec - t1.tv_sec) * 1000000 + t2.tv_usec - t1.tv_usec) / 1000000;
printf("%.4f\n", time);
return 0;
}
每一级递归执行 4 次调用,其中只有第二次和第三次调用可以实际并行运行。
现在我尝试了这个不同的版本:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <omp.h>
struct timeval t1, t2;
void recursive_task(int level)
{
//printf("%d\n", level);
if (level == 0){
usleep(1000);
return;
}
else
{
#pragma omp task if(0)
{
recursive_task(level-1);
#pragma omp task
{
recursive_task(level-1);
}
recursive_task(level-1);
#pragma omp taskwait
recursive_task(level-1);
}
}
}
int main()
{
double time;
gettimeofday(&t1, 0);
#pragma omp parallel
{
#pragma omp single
{
recursive_task(7);
}
}
gettimeofday(&t2, 0);
time = (double)((t2.tv_sec - t1.tv_sec) * 1000000 + t2.tv_usec - t1.tv_usec) / 1000000;
printf("%.4f\n", time);
return 0;
}
执行似乎也遵循正确的调用顺序(我认为这与 if(0) 子句有关)。
然而令我惊讶的是第二个比第一个更快。我使用 8 个线程执行,第一种方法需要 4.4 秒,第二种方法需要 3.4 秒!
使第二个代码更快的主要区别是什么?
重点是影响
#pragma omp taskwait
等待当前任务的子任务完成,当有使用#pragma omp task if(0)
创建的任务时,当前任务不一样。这意味着“不”拥有较晚的指令会导致“过度同步”,从而导致更高的执行时间(当然是由于并行性较低)。事实上,在这种情况下, #pragma omp taskwait
指令会等待更多任务(尤其是没有 if(0)
的真实任务。
2 个级别的示例为了更好地理解发生了什么,我们可以分析该函数在级别 2 上到底做了什么(并将延迟增加到 10_000,以便我们可以在运行时看到任何差异)。这是扁平化代码:
void recursive_task_level2()
{
#pragma omp task if(0)
{
#pragma omp task if(0)
{
usleep(10000);
#pragma omp task
usleep(10000);
usleep(10000);
#pragma omp taskwait
usleep(10000);
}
#pragma omp task // <----- [A] WAITED TASK
{
#pragma omp task if(0)
{
usleep(10000);
#pragma omp task
usleep(10000);
usleep(10000);
#pragma omp taskwait
usleep(10000);
}
}
#pragma omp task if(0) // <----- [B] IMPORTANT ONE
{
usleep(10000);
#pragma omp task
usleep(10000);
usleep(10000);
#pragma omp taskwait // <----- [C]
usleep(10000);
}
#pragma omp taskwait
#pragma omp task if(0)
{
usleep(10000);
#pragma omp task
usleep(10000);
usleep(10000);
#pragma omp taskwait
usleep(10000);
}
}
}
这个想法是:如果
[B]
存在,那么
[C]
不会等待
[A]
,因为它不再是子任务;但是,如果 [B]
不存在,则
[C]
等待 [A]
,即等待 10 毫秒之前。通过在有/没有 [B]
以及有/没有所有其他 #pragma omp task if(0)
的情况下运行此代码来确认此效果。在这种情况下,唯一重要的指令是
[B]
,而注释 [B]
确实会增加执行时间 10 毫秒(92 毫秒 vs 102 毫秒)。这证明了假设是正确的。随着级别的增加,由于过度同步,并行量会大大减少,并且额外的时间也会在关键路径上累积(当然是指数级的),从而导致巨大的执行开销。