OpenMP - 临界区 + 缩减

OpenMP - critical section + reduction

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

code:

#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;
}

输出:

non critical = 50000
critical = 4246432

当然输出是随机的,当涉及到'critical'(变量y)时,另一个表现符合预期并且总是50000。

x 的行为是可以理解的 - reduction 使其在单线程范围内是私有的。在将来自线程的增量值相加并传递给非本地 x 之后。

我不明白的是y的行为。它和 x 一样是私有的,但它也在 critical 部分中,因此它 'has more than one reason' 无法从其他线程访问。然而,我认为,发生的是竞争条件。 critical 是否以某种方式使 y public(共享)?

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

您的代码的主要问题是 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

您的代码只是表现出未定义的行为,critical 的存在与您得到错误的结果无关。

Did the critical somehow made y public (shared)?

不,它没有。它只会通过阻止线程的并发执行来减慢循环。

您缺少的是缩减操作的结果与缩减变量的初始值相结合,即与变量在并行区域之前的值相结合。在您的情况下, xy 都具有随机初始值,因此您将获得随机结果。在您的情况下,初始值 x 恰好为 0,这就是您获得正确结果的原因,因为它只是 UB。初始化 xy 会使您的代码按预期运行。

OpenMP 规范指出:

The reduction clause specifies a reduction-identifier and one or more list items. For each list item, a private copy is created in each implicit task or SIMD lane, and is initialized with the initializer value of the reduction-identifier. After the end of the region, the original list item is updated with the values of the private copies using the combiner associated with the reduction-identifier.

这是使用 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