OpenMP:nowait和reduction子句在同一个pragma上

OpenMP: nowait and reduction clauses on the same pragma

本文关键字:同一个 pragma 子句 reduction nowait OpenMP      更新时间:2023-10-16

我正在学习OpenMP,遇到了以下示例:

#pragma omp parallel shared(n,a,b,c,d,sum) private(i)
{
    #pragma omp for nowait
    for (i=0; i<n; i++)
        a[i] += b[i];
    #pragma omp for nowait
    for (i=0; i<n; i++)
        c[i] += d[i];
    #pragma omp barrier
    #pragma omp for nowait reduction(+:sum)
    for (i=0; i<n; i++)
        sum += a[i] + c[i];
} /*-- End of parallel region --*/

在最后一个for循环中,有一个nowait和一个reduction子句。这是正确的吗?减少条款不需要同步吗?

第二个和最后一个循环中的nowait有些冗余。OpenMP规范在区域结束之前提到nowait,所以这可能会保留在中

但第二个循环前的nowait和第二个环路后的显式势垒相互抵消。

最后,关于sharedprivate子句。在您的代码中,shared没有任何作用,根本不应该使用private:如果您需要一个线程专用变量,只需在并行区域内声明它即可。特别是,您应该在循环内部而不是之前声明循环变量

为了使shared有用,您需要告诉OpenMP默认情况下它不应该共享任何内容。您应该这样做,以避免由于意外共享变量而导致的错误。这是通过指定default(none)来完成的。这给我们留下了:

#pragma omp parallel default(none) shared(n, a, b, c, d, sum)
{
    #pragma omp for nowait
    for (int i = 0; i < n; ++i)
        a[i] += b[i];
    #pragma omp for
    for (int i = 0; i < n; ++i)
        c[i] += d[i];
    #pragma omp for nowait reduction(+:sum)
    for (int i = 0; i < n; ++i)
        sum += a[i] + c[i];
} // End of parallel region

在某些方面,这似乎是一个家庭作业问题,我讨厌为人们做这件事。另一方面,上面的答案并不完全准确,我觉得应该纠正。

首先,虽然在这个例子中不需要共享子句和私有子句,但我不同意Konrad的观点,即不应该使用它们。人们并行代码最常见的问题之一是,他们没有花时间了解变量是如何使用的。不私有化和/或保护共享变量是我看到的最大问题。通过检查变量是如何使用的,并将它们放入适当的共享、私有等子句中,可以大大减少问题的数量。

至于关于障碍的问题,第一个循环可以有一个nowait子句,因为在第二个循环中没有使用计算出的值(a)。只有在计算值之前没有使用计算的值(c)(即没有依赖项),第二个循环才能有nowait子句。在最初的示例代码中,第二个循环上有一个nowait,但在第三个循环之前有一个显式屏障。这很好,因为你的教授试图展示显式屏障的使用——尽管在第二个循环中去掉nowait会使显式屏障变得多余(因为循环末尾有一个隐式屏障)。

另一方面,第二个循环上的nowait和显式屏障可能根本不需要。在OpenMP V3.0规范之前,许多人认为规范中没有澄清的东西是真的。根据OpenMP V3.0规范,在第2.5.1节"回路构造"表2-1"调度条款"中添加了以下内容,即值,静态(调度):

静态时间表的合规实施必须确保将逻辑迭代次数分配给线程将在两个循环中使用区域,如果满足以下条件:1)两个循环区域都具有循环迭代次数相同,2)两个循环区域具有相同的值指定了chunk_size,或者两个循环区域都没有指定chunk_size3)两个环区域结合到相同的平行区域。之间的数据依赖性保证在两个这样的循环中满足相同的逻辑迭代允许安全使用nowait条款(参见第170页A.9节示例)。

现在,在您的示例中,任何循环上都没有显示时间表,因此这可能成立,也可能不成立。原因是,默认时间表是由实现定义的,虽然目前大多数实现都将默认时间表定义为静态的,但这并不能保证。如果你的教授在所有三个循环上都设置了一个没有块大小的静态调度类型,那么nowait可以用于第一个和第二个循环,并且在第二个和第三个循环之间根本不需要任何屏障(隐式或显式)。

现在我们进入第三个循环,以及您关于nowait和reduction的问题。正如Michy所指出的,OpenMP规范允许同时指定(reduction和nowait)。然而,不需要同步就可以完成缩减,这是不正确的。在这个例子中,隐式屏障(在第三个循环的末尾)可以用nowait去除。这是因为在遇到平行区域的隐式屏障之前没有使用减少(和)。

如果您查看OpenMP V3.0规范2.9.3.6节的reduction子句,您会发现以下内容:

如果不使用nowait,则在建筑但是,如果在nowait为的构造上使用reduction子句同样适用,对原始列表项的访问将创建一个竞赛,因此未指定的效果,除非同步确保在所有线程执行了它们的所有迭代或截面构造,以及归约计算已完成并存储该列表项的计算值。这可以最简单地通过屏障同步来确保。

这意味着,如果你想在第三个循环后的并行区域中使用sum变量,那么在使用它之前,你需要一个屏障(隐式或显式)。现在的例子是正确的。

OpenMP规范说:

循环结构的语法如下:

#pragma omp for [clause[[,] clause] ... ] new-line
    for-loops

where子句是以下内容之一:

 ...
 reduction(operator: list)
 ...
 nowait

所以可以有更多的子句,这样既可以有减少语句也可以有无保留语句。

reduction子句中不需要显式同步——由于reduction(+: sum)和之前的屏障力abreduction循环的时间内具有最终值,因此对sum变量的添加是同步的。nowait意味着,如果线程完成了循环中的工作,则不必等到所有其他线程都将完成相同的循环。