通过分离#omp parallel 和#omp for 来减少 OpenMP fork/join 开销

Posted

技术标签:

【中文标题】通过分离#omp parallel 和#omp for 来减少 OpenMP fork/join 开销【英文标题】:Reduce OpenMP fork/join overhead by separating #omp parallel and #omp for 【发布时间】:2014-11-27 15:12:34 【问题描述】:

我正在阅读 Peter S. Pacheco 所著的并行编程简介一书。在 5.6.2 节中,它给出了一个关于减少 fork/join 开销的有趣讨论。 考虑奇偶转置排序算法:

for(phase=0; phase < n; phase++)
    if(phase is even)
#       pragma omp parallel for default(none) shared(n) private(i)
        for(i=1; i<n; i+=2)//meat
    
    else
#       pragma omp parallel for default(none) shared(n) private(i)
        for(i=1; i<n-1; i+=2)//meat
    

作者认为上述代码的 fork/join 开销有些高。因为线程是在外循环的每次迭代中分叉和连接的。因此,他提出以下版本:

# pragma omp parallel default(none) shared(n) private(i, phase)
for(phase=0; phase < n; phase++)
    if(phase is even)
#       pragma omp for
        for(i=1; i<n; i+=2)//meat
    
    else
#       pragma omp for
        for(i=1; i<n-1; i+=2)//meat
    

根据作者的说法,第二个版本在外循环开始之前分叉线程,并在每次迭代中重用线程,从而产生更好的性能。

但是,我怀疑第二个版本的正确性。在我的理解中,#pragma omp parallel 指令会启动一组线程,并让线程并行执行以下结构化块。在这种情况下,结构化块应该是整个外部 for 循环 for(phase=0 ...)。那么,在使用 4 个线程的情况下,不应该是整个外循环执行四次的情况吗?也就是说,如果n=10,那么将在 4 个线程上执行 40 次迭代。我的理解有什么问题? omp parallel(没有 for)如何与上面的以下 for 循环一起玩?

【问题讨论】:

【参考方案1】:

第二个版本是正确的。

根据 OpenMP 规范,#pragma omp parallel for 指令只是 #pragma omp parallel 的快捷方式,紧随其后的是 #pragma omp for,如

#pragma omp parallel

    #pragma omp for
    for(int i=0; i<N; ++i)  /*loop body*/ 

如果在循环构造之前或之后的并行区域中有一些代码,它将由该区域中的每个线程独立执行(除非受到其他 OpenMP 指令的限制)。但是,#pragma omp for 是一个工作共享结构;该指令之后的循环由该区域中的所有线程共享。 IE。它作为单个循环执行,迭代以某种方式跨线程拆分。因此,如果上面的并行区域由 4 个线程执行,那么循环仍然只会执行一次,而不是 4 次。

回到您问题中的示例:阶段循环由每个线程单独执行,但每个阶段迭代的#pragma omp for 表示共享循环的开始。 n=10时,每个线程会进入一个共享循环10次,并执行其中的一部分;所以内部循环不会执行 40 次,而只有 10 次。

注意#pragma omp for 末尾有一个隐含的屏障;这意味着在所有其他线程也完成其部分之前,完成其共享循环部分的线程将不会继续。因此,执行是跨线程同步的。在大多数情况下,这是确保正确性所必需的;例如在您的示例中,这保证了线程始终在同一阶段工作。但是,如果一个区域内的后续共享循环可以安全地同时执行,则可以使用nowait 子句来消除隐式障碍并允许线程立即继续执行并行区域的其余部分。

还要注意,这种工作共享指令的处理是 OpenMP 特有的。使用其他并行编程框架,您在问题中使用的逻辑可能是正确的。

最后,智能 OpenMP 实现不会在并行区域完成后加入线程;相反,线程可能会忙等待一段时间,然后休眠,直到另一个并行区域启动。这样做正是为了防止并行区域开始和结束时的高开销。因此,虽然书中建议的优化仍然消除了一些开销(也许),但对于某些算法,它对执行时间的影响可能可以忽略不计。问题中的算法很可能是其中之一;在第一个实现中,并行区域在串行循环中一个接一个地快速跟随,因此 OpenMP 工作线程很可能会在一个区域的开头处于活动状态并快速启动它,从而避免了 fork/join 开销。因此,如果您在实践中发现与所描述的优化没有任何性能差异,请不要感到惊讶。

【讨论】:

非常全面的答案。 我怀疑,正如你所说,第二个版本不会更有效率。特别是如果n&gt;&gt;nthreads. 您能指出哪些 OpenMP 实现是“智能的”,因此不会在每个并行区域之后重新加入线程吗? @davide,我可以肯定地说英特尔的 OpenMP 实现就是这种情况;它甚至被记录在案(software.intel.com/en-us/node/522682):当一个团队解散时,“团队中的线程进入等待状态,直到需要它们来组建另一个团队”。 Clang 使用英特尔 OpenMP 运行时的开源版本,因此 Clang 也必须如此。我不知道其他实现。

以上是关于通过分离#omp parallel 和#omp for 来减少 OpenMP fork/join 开销的主要内容,如果未能解决你的问题,请参考以下文章

OpenMP:不能同时使用 omp parallel for 和 omp task 吗? /错误:工作共享区域可能没有紧密嵌套在工作共享内

“pragma omp parallel for”中的“omp_get_num_threads”给了我编译错误

嵌套循环中未正确忽略内部循环的 Pragma omp parallel

OMP SECTIONS 和 DO 在同一个 PARALLEL 块中

omp parallel for:如何让线程写入私有数组并在所有线程完成处理后合并所有数组

在另一个并行循环中调用函数时,函数中的“pragma omp parallel for”无效