OpenMP 嵌套循环任务并行性,计数器未给出正确结果

Posted

技术标签:

【中文标题】OpenMP 嵌套循环任务并行性,计数器未给出正确结果【英文标题】:OpenMP nested loop task parallelism, counter not giving correct result 【发布时间】:2022-01-05 01:20:40 【问题描述】:

我是 openMP 的新手。我正在尝试使用任务并行化嵌套循环,但它没有给我正确的计数器输出。顺序输出是“总像素 = 100000000”。谁能帮我解决这个问题?

注意:我已使用 #pragma omp parallel for reduction (+:pixels_inside) private(i,j) 完成此操作。这很好,现在我想使用任务。

到目前为止我所尝试的:

#include<iostream>
#include<omp.h>
using namespace std;

int main()
    int total_steps = 10000;

    int i,j;
    int pixels_inside=0;
    omp_set_num_threads(4);
    //#pragma omp parallel for reduction (+:pixels_inside) private(i,j)
    #pragma omp parallel
    #pragma omp single private(i)
    for(i = 0; i < total_steps; i++)
        #pragma omp task private(j)
        for(j = 0; j < total_steps; j++)
            pixels_inside++;
        
    

    cout<<"Total pixel = "<<pixels_inside<<endl;
    return 0;

【问题讨论】:

【参考方案1】:

正如@tartarus 已经解释的那样,您的代码中有竞争条件,最好通过使用减少来避免它。如果你做什么和#pragma omp parallel for reduction (+:pixels_inside) private(i,j)一样但是使用任务,你必须使用以下:

    #pragma omp parallel 
    #pragma omp single    
    #pragma omp taskloop reduction (+:pixels_inside) private(i,j)
    for(i = 0; i < total_steps; i++)
        for(j = 0; j < total_steps; j++)
            pixels_inside++;
        
    

在这个版本中创建的任务更少,并且使用缩减而不是临界区,因此性能会更好(类似于使用#pragma omp parallel for可以获得的效果)

更新(性能评论):我想这只是一个简化的示例,而不是您要并行化的真实代码。如果性能增益不够好,很可能意味着并行开销大于要做的工作。在这种情况下,请尝试并行化较大部分的代码。请注意,对于任务而言,并行开销通常更大(与 #pragma omp parallel for 相比)。

【讨论】:

请注意,一些 OpenMP 运行时不是很聪明,每次循环迭代会生成 1 个任务,这可能会对性能产生很大影响。幸运的是,任务循环的粒度可以使用附加子句来控制:grainsize 和 num_tasks。 感谢您的澄清。哪个 OpenMP 运行时只生成一个任务?使用最近的 gcc 和 clang 我没有注意到这样的问题。 确实!我认为几年前是 ICC(或可能是 GCC)。这种行为通常在运行时而不是编译器中实现(至少对于 GCC、Clang 和 ICC)。 ICC 像 Clang 一样使用 libOMP,因此他们可能已经改进了这一点。我看到他们在并行循环(包括任务循环)的时间表上“最近”做了一些改变。很高兴看到他们提高了任务循环的性能:)。【参考方案2】:

首先,您需要为 OpenMP 声明您正在使用哪些变量以及它们有哪些保护措施。一般来说,您的代码具有default(shared),因为您没有另外指定。这使得所有线程都可以使用相同的内存位置访问所有变量。 你应该使用这样的东西:

#pragma omp parallel default(none) shared(total_steps, pixels_inside)
[...]
#pragma omp task private(j) default(none) shared(total_steps, pixels_inside)

现在,线程只会使用必要的东西。

其次,主要问题是您没有关键部分保护。这意味着,当线程正在运行时,它们可能希望使用共享变量并且发生竞争条件。例如,您有线程 A 和 B,其中变量 x 均可访问(也称为共享内存变量)。现在让我们说 A 加 2 和 B 加 3 到变量。线程的速度不同,因此可能会发生这种情况,A 取 x=0,B 取 x=0,A 加 0+2,B 加 0+3,B 将数据返回到内存位置 x=3,A 将数据返回到内存位置 x=2。最后 x = 2。pixels_inside 也会发生同样的情况,因为线程接受变量,加 1 并从它得到它的地方返回。为了克服这个问题,您可以使用测量来确保关键部分的保护:

#pragma omp critical

    //Code with shared memory
    pixels_inside++;

reduction 中不需要关键部分保护,因为 recution 参数中的变量具有此保护。

现在您的代码应该如下所示:

#include <iostream>
#include <omp.h>
using namespace std;

int main() 
    int total_steps = 10000;

    int i,j;
    int pixels_inside=0;
    omp_set_num_threads(4);
//#pragma omp parallel for reduction (+:pixels_inside) private(i,j)
#pragma omp parallel default(none) shared(total_steps, pixels_inside)
#pragma omp single private(i)
    for(i = 0; i < total_steps; i++)
#pragma omp task private(j) default(none) shared(total_steps, pixels_inside)
        for(j = 0; j < total_steps; j++)
#pragma omp critical
            
                pixels_inside++;
            
        
    

    cout<<"Total pixel = "<<pixels_inside<<endl;
    return 0;

虽然我建议使用reduction,因为它具有更好的性能和优化此类计算的方法。

【讨论】:

感谢您的回答。这对我来说真的很有意义。但似乎表现不是很好。哪种方法最适合解决此类问题?请给我一些建议。 我会用这个。 2 层减少的瓶盖比单个关键部分少。 #pragma omp parallel for reduction(+:pixels_inside) default(none) shared(total_steps) for(i = 0; i 仅增量的关键部分是可怕的。使用原子更新明显更好。尽管如此,两者在大多数架构上都是按顺序执行的,并且由于内核之间的缓存线弹跳,它们比顺序代码慢。确实,减少要好得多。请注意,代码几乎无法作为评论阅读,我认为编辑答案要好得多(对于未来的读者)。

以上是关于OpenMP 嵌套循环任务并行性,计数器未给出正确结果的主要内容,如果未能解决你的问题,请参考以下文章

OpenMP 如何处理嵌套循环?

使用 OpenMP 在 C、C++ 中并行化嵌套 for 循环的几种方法之间的区别

OpenMP 矩阵乘法嵌套循环

openMP 嵌套并行 for 循环与内部并行 for

在 OpenMP 中并行化嵌套循环并使用更多线程执行内部循环

并行任务中的 C++ OpenMP 变量可见性