对OpenMP reduction子句中的变量执行原子操作

atomic operations on a variable which is in OpenMP reduction clause

本文关键字:变量 执行 原子操作 OpenMP reduction 子句      更新时间:2023-10-16

我有以下一段代码。它的想法很简单。我的完整程序中有数十亿个事件,我需要计算其中一些不使用long-int类型的事件。因此,我必须使用2个int数字HITCOUNT作为1个intnumber的开头,因为会有一个1int变量的溢出(非常大的循环计数)。

#include <fstream>
#include <cstring>
#include <cmath>
#include <random>
#include <limits>
#include <chrono>
using namespace std;
int N=1000000000;
long int K=20*N;
int HIT=0;
int COUNT=0;
long int MAX=std::numeric_limits<int>::max();
int main(int argc, char **argv)
{
auto begin=std::chrono::steady_clock::now();
for(long int i=0; i<K; ++i)
{
++HIT;
if(HIT == MAX)
{
++COUNT;
HIT=0;
cout<<"COUNT="<<COUNT<<endl;
}
}
auto end=std::chrono::steady_clock::now();
cout<<"HIT="<<HIT<<endl;
cout<<"COUNT="<<COUNT<<endl;
const long int Total = HIT+COUNT*MAX;
cout<<"Total="<<Total<<" MAX="<<MAX<<endl;
if(Total==K) cout<<"Total == K"<<endl;
else         cout<<"Total != K"<<endl;
auto elapsed_ms=std::chrono::duration_cast<std::chrono::milliseconds>(end-begin);
std::cout<<"time="<<elapsed_ms.count()<<" ms"<<std::endl;
return 0;
}

该代码在1个线程中正常工作,并给出以下输出:

COUNT=1
COUNT=2
COUNT=3
COUNT=4
COUNT=5
COUNT=6
COUNT=7
COUNT=8
COUNT=9
HIT=672647177
COUNT=9
Total=20000000000 MAX=2147483647
Total == K
time=30971 ms

如果可能的话,我需要让它使用OpenMP并行工作,而不使用互斥或与编译器实现连接的一些函数。但当我将其修改为:

#pragma omp parallel for simd reduction(+:HIT,COUNT)
for(long int i=0; i<K; ++i)

输出如下:

HIT=20000000000
COUNT=0
Total=20000000000 MAX=2147483647
Total == K
time=2771 ms

最后,当我将代码修改为:

#pragma omp parallel for simd reduction(+:HIT,COUNT)
for(long int i=0; i<K; ++i)
{
++HIT;
if(HIT == MAX)
{
++COUNT;
#pragma omp atomic write
HIT=0;
cout<<"COUNT="<<COUNT<<endl;
}
}

输出为:

COUNT=1
COUNT=1
COUNT=1
COUNT=1
COUNT=1
COUNT=1
COUNT=1
COUNT=1
HIT=2820130824
COUNT=8
Total=20000000000 MAX=2147483647
Total == K
time=4232 ms

有人能向我解释一下发生了什么以及为什么输出如此不同吗?

我需要使用OpenMP使代码正确地并行工作,那么如何正确地做到这一点呢?

是否

#pragma omp atomic write

正确还是应该写

#pragma omp atomic update?

是否可以对OpenMPreduction子句中已经存在的值编写原子操作?

使用英特尔C++2019编译器。

g++不允许在中使用simd

#pragma omp parallel for simd reduction(+:HIT,COUNT)

如果要删除simd,则代码使用g++时工作不正确。

简单的+约简对两个不完全独立求和的整数不起作用,但自从OpenMP 4.0以来,您可以声明自己的约简。您所需要做的就是抽象class(或struct)中计数器的两个部分,并定义一个对这些对象求和的函数。在下面的例子中,使用了一个重载的复合赋值运算符(+=):

#include<限制>#包括<iostream>#包括<omp.h>使用命名空间std;const long int MAX=std::numeric_limits<int>:max();const long int K=MAX+20L;类large_count{int计数,命中;公共:large_count():计数(0),命中(0){}//前缀增量运算符large_count&运算符++(){命中++;if(hit==MAX){hit=0;计数++;}return*this;}//复合赋值运算符large_count&运算符+=(const large_count和other){count+=其他计数;long int sum_hit=(long)hit+other.hit;如果(sum_hit>=MAX){计数++;hit=sum_hit-MAX;}其他的hit=sum_hit;return*this;}long total()const{return hit+count*MAX;}};#pragma omp声明减少(large_sum:large_count:omp_out+=omp_in)int main(){large_count cnt;双t=-omp_get_wtime();#用于归约的pragma omp并行(large_sum:cnt)for(long int i=0;i<K;i++)++cnt;t+=omp_get_wtime();cout<lt;(cnt.total()==K"YES":"否")<lt;endl;cout<lt;t<lt"s"<lt;endl;}

自定义减少使用:声明

#pragma omp declare reduction (large_sum : large_count : omp_out += omp_in)

声明有三个部分:

  • large_sum-这是自定义缩减操作的名称
  • large_count-这是还原操作的类型
  • omp_out += omp_in-这是组合器表达式。CCD_ 8和CCD_ 9是由OpenMP运行时提供的特殊伪变量。它们都属于large_count型。组合器表达式必须组合这两个值并更新omp_out的值

样本输出:

$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
...
$ g++ -std=c++11 -fopenmp -o cnt cnt.cc
$ OMP_NUM_THREADS=1 ./cnt
YES
9.39628 s
$ OMP_NUM_THREADS=3 ./cnt
YES
3.79765 s

问题源于每个线程都有自己的HITCOUNT副本。许多线程将以HIT中的大值结束。当循环结束时,由于OpenMP reduce子句,这些被聚合,导致多个";溢出";CCD_ 15。

对所示代码的OpenMP实现的简单修复是包括

COUNT += HIT / MAX;
HIT %= MAX;

循环结束后。

原子写入指令是转移注意力。它改变了循环的时间,导致更多的线程达到溢出限制。

从您的问题描述中,听起来代码中实际的HITint,而不是long int。这更难解决,因为无法使用上面的简单除法计算多个溢出,因为您没有完全计算所有溢出的精度。您还应该考虑使用unsigned而不是有符号的int类型,因为这可能会延迟溢出问题,并且在发生溢出时,可以避免有符号值溢出时出现"未定义行为"。

可能的解决方案包括:

  1. 使用单个MAXCOUNT变量以及受互斥保护的代码块来对这两个值进行非原子递增
  2. 使用单个MAXCOUNT变量(声明为std::atomic)以及fetch_add(或可能的exchange)来处理更新。如果使用unsigned类型,则可以让MAX滚动到0,并在发生滚动时更新COUNT
  3. MAX更改为较小的数字,以使(nThreads * MAX)不超过数字限制
相关文章: