【问题标题】:Fetch-and-add using OpenMP atomic operations使用 OpenMP 原子操作进行获取和添加
【发布时间】:2011-05-01 09:33:59
【问题描述】:

我正在使用 OpenMP,需要使用 fetch-and-add 操作。但是,OpenMP 没有提供适当的指令/调用。我想保持最大的可移植性,因此我不想依赖编译器内在函数。

相反,我正在寻找一种方法来利用 OpenMP 的原子操作来实现这一点,但我遇到了死胡同。这甚至可以做到吗?注意,以下代码几乎可以满足我的要求:

#pragma omp atomic
x += a

几乎——但不完全是,因为我真的需要x 的旧值。 fetch_and_add 应定义为产生与以下相同的结果(仅非锁定):

template <typename T>
T fetch_and_add(volatile T& value, T increment) {
    T old;
    #pragma omp critical
    {
        old = value;
        value += increment;
    }
    return old;
}

(如果我没记错的话,可以询问比较和交换的等效问题,但可以根据另一个问题来实现。)

【问题讨论】:

  • 只是说atomic 并不是它的名字所承诺的那样,因为任何内存被atomic(在任何其他线程上)修改的线程都需要重新-缓存。如此频繁和重复的atomic 可能会影响您的性能(最好使用锁和缓冲竞速写入)。
  • @Walter 这也是我凭经验发现的:无锁算法的性能与使用锁的等效算法相当。无锁算法使用复杂得多的同步——不是在性能方面,而是在逻辑方面(因此有机会引入错误)。

标签: c++ atomic openmp compare-and-swap


【解决方案1】:

从 openmp 3.1 开始支持捕获原子更新,您可以捕获旧值或新值。因为无论如何我们都必须从内存中取值来递增它,所以我们应该能够从一个 CPU 寄存器访问它并将其放入线程私有变量中才有意义。

如果您使用 gcc(或 g++),有一个很好的解决方法,请查找 atomic builtins: http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html

它认为英特尔的 C/C++ 编译器也支持这个,但我没有尝试过。

目前(直到实现 openmp 3.1),我在 C++ 中使用了内联包装函数,您可以在其中选择在编译时使用哪个版本:

template <class T>
inline T my_fetch_add(T *ptr, T val) {
  #ifdef GCC_EXTENSION
  return __sync_fetch_and_add(ptr, val);
  #endif
  #ifdef OPENMP_3_1
  T t;
  #pragma omp atomic capture
  { t = *ptr; *ptr += val; }
  return t;
  #endif
}

更新:我刚刚尝试了 Intel 的 C++ 编译器,它目前支持 openmp 3.1(实现了原子捕获)。英特尔提供在 linux 中免费使用其编译器用于非商业目的:

http://software.intel.com/en-us/articles/non-commercial-software-download/

GCC 4.7 将支持 openmp 3.1,当它最终发布时......希望很快 :)

【讨论】:

  • 无论如何我都使用了 GCC 内置函数,但当然这对于互操作性来说是很糟糕的。感谢 OpenMP 3.1 指针。不幸的是,由于 VC++ 目前甚至不支持 OpenMP 3,所以目前这还只是理论上的。
  • 只是为了完整性:它应该是 #ifdef __GNUC__ ... #elif defined(_OPENMP) and _OPENMP&gt;=201107 (对于 OpenMP 3.1) ... #else #error "Requires gcc or OpenMP&gt;=3.1" #endif。谢谢!
【解决方案2】:

如果要获取 x 的旧值且 a 不变,请使用 (x-a) 作为旧值:

fetch_and_add(int *x, int a) {
 #pragma omp atomic
 *x += a;

 return (*x-a);
}

更新:这不是一个真正的答案,因为 x 可以在原子之后被另一个线程修改。 因此,使用 OMP Pragmas 制作通用的“获取和添加”似乎是不可能的。作为通用,我的意思是操作,可以在任何地方的 OMP 代码中轻松使用。

您可以使用omp_*_lock 函数来模拟原子:

typedef struct { omp_lock_t 锁; int 值;} atomic_simulated_t;

fetch_and_add(atomic_simulated_t *x, int a)
{
  int ret;
  omp_set_lock(x->lock);
  x->value +=a;
  ret = x->value;
  omp_unset_lock(x->lock);
}

这既丑陋又慢(执行 2 个原子操作而不是 1 个)。但是,如果您希望您的代码非常可移植,那么在所有情况下它都不是最快的。

您说“如下(仅非锁定)”。但是“非锁定”操作(使用 CPU 的“LOCK”前缀,或 LL/SC 等)和锁定操作(通过几个原子指令自行实现,忙循环用于短暂等待解锁和操作系统睡眠)有什么区别?等待很长时间)?

【讨论】:

  • 对于 cas - openmp 支持条件原子的变体,但仅在 fortran 中。它是一个 MIN 和 MAX;他们是有条件的。可用于实现 CAS 操作的子集。
  • @Konrad Rudolph,我也是,因为我需要 1 周时间才能得到这个 :)。此外,我需要的步骤是在不同平台上学习 LL/SC 操作。
  • @osgx:这实际上不是问题,因为您可以简单地预先将类型强制转换为适当位大小的整数。当然,这是 UB 和平台相关的,但如果有适当的保护,这是一种非常可靠的技术。
  • 该死,这个答案也不正确:在原子更新和最后一行之间,另一个线程当然可能会修改x
  • @Konrad Rudolph,是的,第一次尝试是错误的。更新了答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-04
  • 1970-01-01
  • 2014-03-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多