c++ - #pragma omp atomic 与 OMP_NUM_THREADS=1 的性能问题

标签 c++ openmp atomic pragma single-threaded

我观察到我正在编写的 openmp 代码的意外(对我来说!)行为。代码结构如下:

#pragma omp parallel for
for(int i=0;i<N;i++){ 
 // lots of calculations that produce 3 integers i1,i2,i3 and 3 doubles d1,d2,d3 
 #pragma omp atomic 
 J1[i1] += d1;
 #pragma omp atomic
 J2[i2] += d2; 
 #pragma omp atomic
 J3[i3] += d3; 
}

我编译了这段代码的三个不同版本:

1) 使用 openmp (-fopenmp)

2) 没有openmp

3) 使用 openmp,但没有 3 个原子操作(只是作为测试,因为原子操作是必需的)

当我使用环境变量 OMP_NUM_THREADS=1 运行版本 1) 时,我观察到相对于版本 2) 的速度明显下降);而版本 3) 运行速度与版本 2) 一样快。

我想知道这种行为的原因(为什么原子操作即使是单线程也会减慢代码速度?!)以及是否有可能以版本 1) 运行的方式编译/重写代码与版本 2 一样快)。

我在问题的末尾附上了一个显示上述行为的工作示例。我编译了 1) :

g++ -fopenmp -o toy_code toy_code.cpp -std=c++11 -O3

2)与:

g++ -o toy_code_NO_OMP toy_code.cpp -std=c++11 -O3

和 3) 与:

g++ -fopenmp -o toy_code_NO_ATOMIC toy_code_NO_ATOMIC.cpp -std=c++11 -O3

编译器的版本是gcc version 5.3.1 20160519 (Debian 5.3.1-20)。 3个版本的执行时间为:

1) 1 分 24 秒

2) 51 秒

3) 51 秒

提前感谢您的任何建议!

// toy_code.cpp 
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include <cmath>
#include <omp.h>
#define Np 1000000
#define N 1000

int main (){
        double* Xp, *Yp, *J,*Jb;
        Xp = new double[Np];
        Yp = new double[Np];  
        J = new double [N*N];
        Jb = new double [N*N];

        for(int i=0;i<N*N;i++){
            J[i]=0.0;
            Jb[i]=0.0;
        }

        for(int i=0;i<Np;i++){
            Xp[i] = rand()*1.0/RAND_MAX - 0.5;
            Yp[i] = rand()*1.0/RAND_MAX - 0.5;
        }

        for(int n=0; n<2000; n++){
        #pragma omp parallel for
        for(int p=0;p<Np;p++){
            double rx = (Xp[p]+0.5)*(N-1);
            double ry = (Yp[p]+0.5)*(N-1);
            int xindex = (int)floor(rx+0.5);
            int yindex = (int)floor(ry+0.5);
            int k;
            k=xindex*N+yindex;

            #pragma omp atomic
            J[k]+=1;
            #pragma omp atomic
            Jb[k]+=1;
         }
         }

        delete[] Xp;
        delete[] Yp;
        delete[] J;
        delete[] Jb;

return 0;
}

最佳答案

如果启用 OpenMP,gcc 必须生成不同的代码,这些代码适用于仅在运行时才知道的任意数量的线程。

在这种特殊情况下,请查看 gcc -S 的输出(由标签略微缩短)。

没有 OpenMP:

.loc 1 38 0 discriminator 2  # Line 38 is J[k]+=1;
movsd   8(%rsp), %xmm1
cvttsd2si   %xmm0, %edx
cvttsd2si   %xmm1, %eax
movsd   .LC3(%rip), %xmm0
imull   $1000, %eax, %eax
addl    %edx, %eax
cltq
salq    $3, %rax
leaq    0(%r13,%rax), %rdx
.loc 1 40 0 discriminator 2   # Line 40 is Jb[k]+=1;
addq    %r12, %rax
.loc 1 29 0 discriminator 2
cmpq    $8000000, %r15
.loc 1 38 0 discriminator 2
addsd   (%rdx), %xmm0
movsd   %xmm0, (%rdx)
.loc 1 40 0 discriminator 2
movsd   .LC3(%rip), %xmm0
addsd   (%rax), %xmm0
movsd   %xmm0, (%rax)

展开循环使这变得相当复杂。

使用-fopenmp:

movsd   (%rsp), %xmm2
cvttsd2si   %xmm0, %eax
cvttsd2si   %xmm2, %ecx
imull   $1000, %ecx, %ecx
addl    %eax, %ecx
movslq  %ecx, %rcx
salq    $3, %rcx
movq    %rcx, %rsi
addq    16(%rbp), %rsi
movq    (%rsi), %rdx
movsd   8(%rsp), %xmm1
jmp .L4
movq    %rax, %rdx
movq    %rdx, (%rsp)
movq    %rdx, %rax
movsd   (%rsp), %xmm3
addsd   %xmm1, %xmm3
movq    %xmm3, %rdi
lock cmpxchgq   %rdi, (%rsi)
cmpq    %rax, %rdx
jne .L9
.loc 1 40 0
addq    24(%rbp), %rcx
movq    (%rcx), %rdx
jmp .L5
.p2align 4,,10
.p2align 3
movq    %rax, %rdx
movq    %rdx, (%rsp)
movq    %rdx, %rax
movsd   (%rsp), %xmm4
addsd   %xmm1, %xmm4
movq    %xmm4, %rsi
lock cmpxchgq   %rsi, (%rcx)
cmpq    %rax, %rdx
jne .L10
addq    $8, %r12
cmpq    %r12, %rbx
jne .L6

我不打算尝试解释或理解这里发生的所有细节,但这不是消息所必需的:编译器必须使用可能更昂贵的不同原子指令,尤其是 lock cmpxchgq.

除了这个基本问题之外,OpenMP 可能会以任何可以想象的方式扰乱优化器,例如干扰展开。我还看到了一个奇怪的案例,其中英特尔编译器实际上为 OpenMP 循环生成了更高效的串行代码。

附言认为自己很幸运——情况可能更糟。如果编译器不能将原子指令映射到硬件指令,它必须使用锁,这会更慢。

关于c++ - #pragma omp atomic 与 OMP_NUM_THREADS=1 的性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37573265/

相关文章:

Java 数组 : synchronized + Atomic*, 或同步就足够了吗?

c++ - 什么是 ATOMIC_FLAG_INIT 以及它应该如何使用?

c++ - 编译器属性卡在函数类型上——是否有针对此 clang-cl 错误的解决方法?

c - 优化嵌套循环

c - 当我尝试编译这个程序时,我在程序错误中遇到了 '#'

c++ - 原子比较运算符(无交换)

c++ - 使用 Qt 5.8 构建 Tesseract OCR

c++ - 未解析的外部符号 __mm256_setr_epi64x

c++ - Apache Thrift 仅用于处理,而不是服务器

c - 使用 openMP 并行化嵌套 for 循环