OpenMP 如何处理嵌套循环?

Posted

技术标签:

【中文标题】OpenMP 如何处理嵌套循环?【英文标题】:How does OpenMP handle nested loops? 【发布时间】:2012-11-01 15:57:19 【问题描述】:

以下代码是仅并行化第一个(外部)循环,还是并行化整个嵌套循环?

    #pragma omp parallel for
    for (int i=0;i<N;i++)
     
      for (int j=0;j<M;j++)
      
       //do task(i,j)//
      
    

我只是想确定上面的代码是否将并行化整个嵌套的 for 循环(因此一个线程直接相关任务(i,j)),或者它只并行化外部 for 循环(因此它确保,对于每个循环索引为 i 的并行线程,其内部循环将在单个线程中按顺序完成,这是非常重要的)。

【问题讨论】:

【参考方案1】:

您编写的行将仅并行化外循环。要并行化两者,您需要添加 collapse 子句:

#pragma omp parallel for collapse(2)
    for (int i=0;i<N;i++)
     
      for (int j=0;j<M;j++)
      
       //do task(i,j)//
      
    

您可能需要查看OpenMP 3.1 规范(第 2.5.1 节)了解更多详情。

【讨论】:

谢谢,太好了,我只想并行外部循环,同时确保内部循环按顺序完成工作。 在这种情况下我是否需要将ij 声明为私有? @giannis gonidelis,不,你没有。有人告诉我这样做是一种很好的做法,但我个人不喜欢这样做 - 因为不将 i 和 j 声明为私有更容易/更简单。 i 是循环索引,因此默认为私有。对于j,这将是两种不同的情况。如果j 在 for 循环之外声明,它将是公共的,您应该将其声明为私有以确保答案正确。如果 j 在 for 循环中声明,它将是私有的,因此无需手动声明。【参考方案2】:

通过以下示例,您将能够更好地理解这一点。 让我们用两个线程来做。

#pragma omp parallel for num_threads(2)
for(int i=0; i< 3; i++) 
    for (int j=0; j< 3; j++) 
        printf("i = %d, j= %d, threadId = %d \n", i, j, omp_get_thread_num());
    

那么结果就是,

i = 0, j= 0, threadId = 0 
i = 0, j= 1, threadId = 0 
i = 0, j= 2, threadId = 0 
i = 1, j= 0, threadId = 0 
i = 1, j= 1, threadId = 0 
i = 1, j= 2, threadId = 0 
i = 2, j= 0, threadId = 1 
i = 2, j= 1, threadId = 1 
i = 2, j= 2, threadId = 1

这意味着,当您将#pragma omp parallel for 添加到最上面的for 循环时,该for 循环的索引将在线程之间划分。如您所见,当 i 的索引相同时,线程 id 也相同。

取而代之的是,我们可以并行嵌套 for 循环中的组合。在这个例子中,我们可以有以下 i 和 j 的组合。

i = 0, j= 0
i = 0, j= 1
i = 0, j= 2
i = 1, j= 0
i = 1, j= 1
i = 1, j= 2
i = 2, j= 0
i = 2, j= 1
i = 2, j= 2

为了使代码组合更加并行化,我们可以添加折叠关键字,如下所示。

#pragma omp parallel for num_threads(2) collapse(2)
for(int i=0; i< 3; i++) 
    for (int j=0; j< 3; j++) 
        printf("i = %d, j= %d, threadId = %d \n", i, j, omp_get_thread_num());
    

那么结果如下。

i = 0, j= 0, threadId = 0 
i = 0, j= 1, threadId = 0 
i = 1, j= 2, threadId = 1 
i = 2, j= 0, threadId = 1 
i = 2, j= 1, threadId = 1 
i = 2, j= 2, threadId = 1 
i = 0, j= 2, threadId = 0 
i = 1, j= 0, threadId = 0 
i = 1, j= 1, threadId = 0 

然后你可以看到,与之前不同的是,对于同一个索引 i,可以有不同的线程 id(when (i=1 and j=2 threadId=1) 也可以是 (i=1 and j=0 threadId=0) )。这意味着在这种情况下,i 和 j 的组合在线程之间进行划分。

【讨论】:

在正确嵌套循环的情况下,外部循环并行化通常是最好的。如果外层循环数与线程数相比并不大,如果可以使嵌套循环符合条件,并且不干扰 simd 矢量化等内层循环优化,则上述折叠是一个很好的方法。 由于数据依赖性,并非所有循环都是可折叠的,所以一般的答案是否定的,因为嵌套并行并不适用于每个循环,这就是为什么人们更多地转向 GPU 的三个层次并行性

以上是关于OpenMP 如何处理嵌套循环?的主要内容,如果未能解决你的问题,请参考以下文章

嵌套循环的 OpenMP SIMD 矢量化

OpenMP 矩阵乘法嵌套循环

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

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

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

用于嵌套 for 循环的 OpenMP?