OpenMP - 临界区 + 归约

5

我正在学习使用C和OpenMP进行并行编程。我想编写一个简单的代码,其中多个线程会递增两个共享值。首先,我使用了reduction指令,并且它按照预期工作。然后,我切换到使用critical指令来启动关键部分 - 它也起作用了。出于好奇心,我尝试合并这两个解决方案并检查行为。我期望得到两个有效的、相等的值。

代码:

#include <stdio.h>
#include <stdlib.h>
#include "omp.h"

#define ITER 50000

int main( void )
{
    int x, y;
    #pragma omp parallel reduction(+:x,y)
    {
       #pragma omp for
       for (int i = 0; i < ITER; i++ )  
       {
            x++;
            #pragma omp critical
            y++;
       }
    }

    printf("non critical = %d\ncritical = %d\n", x, y);
    return 0;
}

输出:

非关键 = 50000
关键 = 4246432

当涉及到 'critical'(变量y)时,输出当然是随机的,而另一个则如预期般始终为50000。

x的行为是可以理解的——reduction使其在单个线程的范围内私有化。在增量值从线程中汇总并传递到非本地x之后。

我不明白的是y的行为。它与x一样是私有的,但它也位于critical部分中,因此它“有多个原因”无法被其他线程访问。然而,我认为发生了竞争条件。 critical是否以某种方式使y公开(共享)?

我知道这段代码没有意义,因为只使用reduction / critical之一就足够了。我只想知道背后的行为是什么。


4
谁初始化这些变量?看起来像是未定义行为。 - 2501
OpenMP 在 reduction 指令中自动完成(对于 + 和 - 为 0,对于 * 和 / 为 1)。 - user4433856
3
对于私有副本,但不适用于可见变量。如果进行初始化会发生什么? - 2501
我相信这个缩减隐含地将被缩减的值私有化了。 - user4433856
@nullPointer 你在技术上是正确的。但是为什么要依赖于OpenMP的隐式行为,当你可以详细说明,并完全确信你所编写的代码正按照你的期望工作呢? - NoseKnowsAll
我非常赞同 - 写更多的代码以避免假设并使事情变得简单明了是更好的选择。然而,在使用 OpenMP 这个新玩具时,我在某些时候获得了一些行为我不期望的代码。像这样的例子在我看来是最有价值的,因为它们给你一点经验。即使只是基本的增量操作。 - user4433856
2个回答

8
您的代码表现出未定义行为,而critical的存在与您得到错误结果无关。
critical”是否使y变成了公共(共享)变量?
不是的。它只通过防止线程的并发执行来减慢循环速度。
您忽略了一个事实,即归约操作的结果与归约变量的初始值相结合,即与变量在并行区域之前的值相结合。在您的情况下,xy都有随机的初始值,因此您得到了随机的结果。初始值x恰好为0,因此您得到了正确的结果,这只是未定义行为。初始化xy将使您的代码按预期运行。
OpenMP规范说明:
reduction”子句指定一个归约标识符和一个或多个列表项。对于每个列表项,在每个隐式任务或SIMD lane中创建一个私有副本,并使用归约标识符的初始化器值进行初始化。在区域结束后,原始列表项使用与归约标识符相关联的组合器更新私有副本的值。
以下是使用4个线程执行您原始代码的结果:
$ icc -O3 -openmp -std=c99 -o cnc cnc.c
$ OMP_NUM_THREADS=1 ./cnc
non critical = 82765
critical = 50000
$ OMP_NUM_THREADS=4 ./cnc
non critical = 82765
critical = 50000
$ OMP_NUM_THREADS=4 ./cnc
non critical = 50000
critical = 50000
$ OMP_NUM_THREADS=4 ./cnc
non critical = 82765
critical = 50194
$ OMP_NUM_THREADS=4 ./cnc
non critical = 82767
critical = 2112072800

第一个单线程运行结果表明它不是由于数据竞争引起的。

使用int x=0, y=0;

$ icc -O3 -openmp -std=c99 -o cnc cnc.c
$ OMP_NUM_THREADS=4 ./cnc
non critical = 50000
critical = 50000
$ OMP_NUM_THREADS=4 ./cnc
non critical = 50000
critical = 50000
$ OMP_NUM_THREADS=4 ./cnc
non critical = 50000
critical = 50000
$ OMP_NUM_THREADS=4 ./cnc
non critical = 50000
critical = 50000

7

您的代码主要问题在于 xy 没有被初始化。第二个问题是关键部分使用的变量应该是 shared 而不是一个缩减变量,虽然这只会影响性能,而不是正确性。

我已经纠正了您的代码,并进行了修改,以演示如何使用 reducecriticalatomic 三者产生相同的结果。

来源

#include <stdio.h>
#include <stdlib.h>
#include <omp.h>

int main(int argc, char* argv[])
{
    int iter = (argc>1) ? atoi(argv[1]) : 50000;
    int r=0, c=0, a=0;

    printf("OpenMP threads = %d\n", omp_get_max_threads() );

    #pragma omp parallel reduction(+:r) shared(c,a)
    {
        #pragma omp for
        for (int i = 0; i < iter; i++ ) {
            r++;
            #pragma omp critical
            c++;
            #pragma omp atomic
            a++;
        }
    }
    printf("reduce   = %d\n"
           "critical = %d\n"
           "atomic   = %d\n", r, c, a);
    return 0;
}

编译

icc -O3 -Wall -qopenmp -std=c99 redcrit.c

输出

OpenMP threads = 4
reduce   = 50000
critical = 50000
atomic   = 50000

1
谢谢你的出色回答!双重计数解释了很多问题。然而,我对你提到的“未定义行为”并不完全确定。我在大学里上这门课,我们的导师从来没有初始化过减少的值 - 我被告知它们由OpenMP用0或1进行初始化,具体取决于减少的运算符。说实话,除了上面的情况,我也是这么做的,一直都有效。编辑:尽管如此,我必须说,当我初始化x、y的值而没有改变其他任何东西时,代码就像@2501建议的那样工作。 - user4433856
1
是的,我知道OpenMP为约简变量定义了默认初始化程序。然而,ISO C没有本地变量的默认初始化程序(https://dev59.com/WHI-5IYBdhLWcg3w9tdw#1597426),这意味着如果未启用OpenMP,则您的程序具有未定义的行为。您不想要那个。此外,“critical”变量没有默认初始化程序。 - Jeff Hammond
我认为你最后一句话对于我的问题也非常关键。再次感谢你帮助我理解它 :) - user4433856
@nullPointer 确实,我没有明确指出,但是是的,那可以解释你的结果。感谢你注意到了这一点。 - Jeff Hammond
2
你的第一句话让我感到毫无意义。除了减慢代码速度外,在 critical 中存在不会改变缩减属性。代码简单地在 xy 没有初始化的情况下是未定义行为。在进入 parallel 区域之前将它们设置为 0 将会导致正确的输出结果。缩减变量的私有副本被初始化为零,但是缩减的结果会被添加到区域之前变量的值上,从而导致未定义行为。 - Hristo Iliev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接