【问题标题】:atomic operations on a variable which is in OpenMP reduction clause对 OpenMP 缩减子句中的变量进行原子操作
【发布时间】:2020-06-22 15:15:08
【问题描述】:

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

#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?

是否可以对 OpenMP reduction 子句中已经存在的值编写 atomic 操作?

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

g++不允许在

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

如果删除 simd,代码使用 g++ 无法正常工作。

【问题讨论】:

  • 我猜由于每个并行线程都有自己的HIT 变量(最初为零),并处理整个范围的子集(K / 线程数),你的条件@987654331 @ 不会发生。如果您改为检查((i + 1) % MAX) == 0,您可能不会遇到此问题。顺便说一句,您的代码确实在 GCC 10.1 上使用 simd 编译。
  • 每个线程都有自己的 HIT 和 COUNT 变量副本(对于#pragma om parallel for 子句是私有的)。这就是为什么在最后一种情况下所有行都是 COUNT=1 的原因。但是,最后,对于顺序代码,COUNT=8 而不是 COUNT=9。为什么?
  • 不幸的是,我不明白为什么使用 ((i+1)%MAX)==0 更好。而且我不能在我的真实代码中使用它。这只是一个小的复制示例。在实际代码中,我应该处理类似于 HIT 和 COUNT 的变量。
  • 我有一个旧版本的 GCC 5.3.1。

标签: c++ openmp atomic


【解决方案1】:

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

#include 
#include 
#include 

使用命名空间标准;

const long int MAX = std::numeric_limits::max();
const long int K = MAX + 20L;

类大计数{
   整数计数,命中;
上市:
   大计数():计数(0),命中(0){}

   // 前缀增量运算符
   large_count& 运算符++() {
      击中++;
      如果(命中 == MAX){
         命中 = 0;
         计数++;
      }
      返回*这个;
   }

   // 复合赋值运算符
   large_count& operator+=(const large_count& other) {
      计数+=其他。计数;
      long int sum_hit = (long)hit + other.hit;
      如果 (sum_hit >= MAX) {
         计数++;
         命中 = sum_hit - MAX;
      }
      别的
         命中 = sum_hit;
      返回*这个;
   }

   long total() const { 返回命中 + 计数 * MAX; }
};

#pragma omp 声明减少 (large_sum : large_count : omp_out += omp_in)

int main() {
   large_count 计数;
   双 t = -omp_get_wtime();
   #pragma omp parallel for reduction(large_sum : cnt)
   for (long int i = 0; i 

自定义归约声明使用:

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

声明分为三部分:

  • large_sum - 这是自定义归约操作的名称
  • large_count - 这是减少操作的类型
  • omp_out += omp_in - 这是组合器表达式。 omp_outomp_in 是 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

【讨论】:

  • 在哪里可以找到有关 imp_out 和 omp_in 的信息以及如何使用它们?
  • OpenMP specification。请注意,链接的规范是 OpenMP 5.0,而您的 GCC 版本支持 OpenMP 4.0。后者没有方便的 HTML 版本,但 PDF 是here。请参阅第 2.14.3.6 节。
【解决方案2】:

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

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

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

循环结束后。

原子写入指令是一条红鲱鱼。它改变了循环的时间,导致更多的线程达到溢出限制。

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

可能的解决方案包括:

  1. 使用单个 MAXCOUNT 变量和受互斥体保护的代码块来执行这两个值的非原子增加。
  2. 使用声明为std::atomic 的单个MAXCOUNT 变量以及fetch_add(或者可能是exchange)来处理更新。如果您使用unsigned 类型,您可以让MAX 翻转为0,并在翻转发生时更新COUNT
  3. MAX 更改为较小的数字,这样(nThreads * MAX) 不会超过数字限制。

【讨论】:

  • 哦,对不起。当然,HIT 和 COUNT 是 int 变量。在上面的代码中更正了它。这就是为什么我不能使用 COUNT += HIT / MAX;并且命中 %= MAX;循环结束后,因为 int 变量 HIT 会溢出。
猜你喜欢
  • 2012-04-26
  • 1970-01-01
  • 1970-01-01
  • 2011-05-01
  • 1970-01-01
  • 2013-02-13
  • 1970-01-01
  • 1970-01-01
  • 2016-05-20
相关资源
最近更新 更多