OpenMP atomic 比对数组的关键速度要慢得多

Posted

技术标签:

【中文标题】OpenMP atomic 比对数组的关键速度要慢得多【英文标题】:OpenMP atomic substantially slower than critical for array 【发布时间】:2021-12-29 14:36:23 【问题描述】:

我在 OpenMP 的 omp atomic 中看到的示例通常涉及更新标量,并且通常报告它比 omp critical 快。在我的应用程序中,我希望更新已分配数组的元素,不同线程将更新的元素之间有一些重叠,我发现原子比临界慢得多。它是一个数组是否有区别,我是否正确使用它?

#include <stdlib.h>
#include <assert.h>
#include <omp.h>

#define N_EACH 10000000
#define N_OVERLAP 100000

#if !defined(OMP_CRITICAL) && !defined(OMP_ATOMIC)
#error Must define OMP_CRITICAL or OMP_ATOMIC
#endif
#if defined(OMP_CRITICAL) && defined(OMP_ATOMIC)
#error Must define only one of either OMP_CRITICAL or OMP_ATOMIC
#endif

int main(void) 

  int const n = omp_get_max_threads() * N_EACH -
                (omp_get_max_threads() - 1) * N_OVERLAP;
  int *const a = (int *)calloc(n, sizeof(int));

#pragma omp parallel
  
    int const thread_idx = omp_get_thread_num();
    int i;
#ifdef OMP_CRITICAL
#pragma omp critical
#endif /* OMP_CRITICAL */
    for (i = 0; i < N_EACH; i++) 
#ifdef OMP_ATOMIC
#pragma omp atomic update
#endif /* OMP_ATOMIC */
      a[thread_idx * (N_EACH - N_OVERLAP) + i] += i;
    
  

/* Check result is correct */
#ifndef NDEBUG
  
    int *const b = (int *)calloc(n, sizeof(int));
    int thread_idx;
    int i;
    for (thread_idx = 0; thread_idx < omp_get_max_threads(); thread_idx++) 
      for (i = 0; i < N_EACH; i++) 
        b[thread_idx * (N_EACH - N_OVERLAP) + i] += i;
      
    
    for (i = 0; i < n; i++) 
      assert(a[i] == b[i]);
    
    free(b);
  
#endif /* NDEBUG */

  free(a);

请注意,在这个简化的示例中,我们可以提前确定哪些元素会重叠,因此在更新这些元素时只应用 atomic/critical 会更有效,但在我的实际应用程序中这是不可能的。

当我使用以下代码编译时:

gcc -O2 atomic_vs_critical.c -DOMP_CRITICAL -DNDEBUG -fopenmp -o critical gcc -O2 atomic_vs_critical.c -DOMP_ATOMIC -DNDEBUG -fopenmp -o atomic

并使用time ./critical 运行我得到: real 0m0.110s user 0m0.086s sys 0m0.058s

time ./atomic,我得到: real 0m0.205s user 0m0.742s sys 0m0.032s

所以它在关键部分使用了大约一半的挂钟时间(当我重复它时,我得到了同样的结果)。

还有另一篇帖子claims critical is slower than atomic,但它使用标量,当我运行提供的代码时,原子结果实际上比关键结果略快。

【问题讨论】:

【参考方案1】:

您的比较不公平:#pragma omp critical 放置在 for 循环之前,因此编译器可以向量化您的循环,但 #pragma omp atomic update 在循环内,这会阻止向量化。矢量化的这种差异导致了令人惊讶的运行时间。为了在循环内进行公平比较:

for (i = 0; i < N_EACH; i++) 
#ifdef OMP_CRITICAL
#pragma omp critical
#endif /* OMP_CRITICAL */
#ifdef OMP_ATOMIC
#pragma omp atomic update
#endif /* OMP_ATOMIC */
   a[thread_idx * (N_EACH - N_OVERLAP) + i] += i;

由于这个向量化问题,如果你只使用单线程,你的真实程序的运行时间很可能是最短的。

【讨论】:

谢谢你的想法,你说得对,当它在循环中时,它的速度会变慢。不过,我不确定我是否同意我的比较“不公平”:我相信这是在这种情况下使用关键和原子的方式,因此在实践中,对于这种情况,关键更快。我不能使用单个线程,因为在我的实际应用程序中,并行部分中的其他计算确实受益于多线程。 好的,不客气。你为什么不使用 -O3 (或 -Ofast),例如-mavx2 编译器标志? 我同意你的评价。扩大成本差异:原子更新将编译为lock add 指令。在当前的 Intel CPU 上,每 18 个时钟周期的吞吐量为 1,这甚至还没有考虑到对缓存的负面影响。关键部分改为编译成 SSE2 或 AVX paddd 指令,每个时钟周期的吞吐量为 4-16 个整数。因此,如果数组足够大,临界区的成本与每线程性能的巨大差异相比相形见绌,尤其是对于低线程数。

以上是关于OpenMP atomic 比对数组的关键速度要慢得多的主要内容,如果未能解决你的问题,请参考以下文章

几乎相同的代码运行速度要慢得多

为啥我使用 openMP atomic 的并行代码比串行代码花费更长的时间?

openmp 共享数组

openMP增量在线程之间添加int?

OpenMP 实现缩减

对于非常接近零的值,双重计算运行速度要慢得多