OpenMP:堆数组性能差(栈数组正常工作)

21

我是一个经验相对较丰富的OpenMP用户,但我遇到了一个令人困惑的问题,并希望有人能够帮助解决。问题在于,一个简单的哈希算法对于栈分配的数组表现良好,但对于堆上的数组表现不佳。

下面的例子使用i%M(i模M)来计数每个数组元素中第M个整数。为简单起见,假设N=1000000,M=10。如果N%M==0,则结果应该是bins[]的每个元素都等于N/M:

#pragma omp for
  for (int i=0; i<N; i++) 
    bins[ i%M ]++;

每个线程都拥有私有的数组bins[](我在关键区域之后对所有线程的结果求和)。

当bins[]在堆栈上分配时,程序表现良好,性能与核心数成比例增长。

然而,如果bins[]在堆上(指向bins[]的指针在堆栈上),性能会急剧下降。 这是一个重大问题!

我想使用OpenMP将某些数据的binning(哈希)并行到堆数组中,但这会导致严重的性能问题。

这绝对不是诸如所有线程尝试写入同一内存区域之类的愚蠢问题。这是因为每个线程都有自己的bins[]数组,无论是堆还是栈上分配的bins,结果都是正确的,并且在单线程运行时性能没有区别。 我在不同的硬件(Intel Xeon和AMD Opteron),使用GCC和Intel C ++编译器复制了这个问题。 所有测试都在Linux(Ubuntu和RedHat)上进行。

好像没有理由使OpenMP的良好性能局限于栈数组。

有什么猜测吗? 也许线程对堆的访问经过某种共享网关? 我该如何解决这个问题?

以下是完整的程序示例:

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

int main(const int argc, const char* argv[])
{
  const int N=1024*1024*1024;
  const int M=4;
  double t1, t2;
  int checksum=0;

  printf("OpenMP threads: %d\n", omp_get_max_threads());

  //////////////////////////////////////////////////////////////////
  // Case 1: stack-allocated array
  t1=omp_get_wtime();
  checksum=0;
#pragma omp parallel
  { // Each openmp thread should have a private copy of 
    // bins_thread_stack on the stack:
    int bins_thread_stack[M];
    for (int j=0; j<M; j++) bins_thread_stack[j]=0;
#pragma omp for
    for (int i=0; i<N; i++) 
      { // Accumulating every M-th number in respective array element
        const int j=i%M;
        bins_thread_stack[j]++;
      }
#pragma omp critical
    for (int j=0; j<M; j++) checksum+=bins_thread_stack[j];
  }
  t2=omp_get_wtime();
  printf("Time with stack array: %12.3f sec, checksum=%d (must be %d).\n", t2-t1, checksum, N);
  //////////////////////////////////////////////////////////////////

  //////////////////////////////////////////////////////////////////
  // Case 2: heap-allocated array
  t1=omp_get_wtime();
  checksum=0;
  #pragma omp parallel 
  { // Each openmp thread should have a private copy of 
    // bins_thread_heap on the heap:
    int* bins_thread_heap=(int*)malloc(sizeof(int)*M); 
    for (int j=0; j<M; j++) bins_thread_heap[j]=0;
  #pragma omp for
    for (int i=0; i<N; i++) 
      { // Accumulating every M-th number in respective array element
        const int j=i%M;
        bins_thread_heap[j]++;
      }
  #pragma omp critical
    for (int j=0; j<M; j++) checksum+=bins_thread_heap[j];
    free(bins_thread_heap);
  }
  t2=omp_get_wtime();
  printf("Time with heap  array: %12.3f sec, checksum=%d (must be %d).\n", t2-t1, checksum, N);
  //////////////////////////////////////////////////////////////////

  return 0;
}

程序的示例输出如下:

对于OMP_NUM_THREADS=1

OpenMP threads: 1
Time with stack array: 2.973 sec, checksum=1073741824 (must be 1073741824).
Time with heap  array: 3.091 sec, checksum=1073741824 (must be 1073741824).

对于OMP_NUM_THREADS=10

OpenMP threads: 10
Time with stack array: 0.329 sec, checksum=1073741824 (must be 1073741824).
Time with heap  array: 2.150 sec, checksum=1073741824 (must be 1073741824).

我非常感激任何帮助!

2个回答

26

这是一个有趣的问题:使用上述代码(gcc4.4,Intel i7),使用4个线程我得到以下输出:

OpenMP threads: 4
Time with stack array:        1.696 sec, checksum=1073741824 (must be 1073741824).
Time with heap  array:        5.413 sec, checksum=1073741824 (must be 1073741824).

但是如果我将malloc行更改为

    int* bins_thread_heap=(int*)malloc(sizeof(int)*M*1024);

更新:或者甚至

    int* bins_thread_heap=(int*)malloc(sizeof(int)*16);

)

然后我得到
OpenMP threads: 4
Time with stack array:        1.578 sec, checksum=1073741824 (must be 1073741824).
Time with heap  array:        1.574 sec, checksum=1073741824 (must be 1073741824).

这里的问题是伪共享。默认的malloc非常(空间上)高效,将请求的小分配都放在一个内存块中,相邻的分配彼此挨着;但由于分配如此之小,以至于多个分配适合同一缓存行,这意味着每当一个线程更新其值时,它会弄脏相邻线程的值的缓存行。通过使请求的内存足够大,这就不再是问题了。
顺便说一下,很明显为什么栈分配的情况不会出现这个问题;不同的线程-不同的堆栈-内存相隔足够远,因此不存在虚假共享问题。
作为一个副点-对于你在这里使用的大小为M的情况并不重要,但如果你的M(或线程数)更大,omp关键字将成为一个大的串行瓶颈;你可以使用OpenMP reductions更有效地对校验和进行求和。
#pragma omp parallel reduction(+:checksum)
    { // Each openmp thread should have a private copy of 
        // bins_thread_heap on the heap:
        int* bins_thread_heap=(int*)malloc(sizeof(int)*M*1024);
        for (int j=0; j<M; j++) bins_thread_heap[j]=0;
#pragma omp for
        for (int i=0; i<N; i++)
        { // Accumulating every M-th number in respective array element
            const int j=i%M;
            bins_thread_heap[j]++;
        }
        for (int j=0; j<M; j++)
            checksum+=bins_thread_heap[j];
        free(bins_thread_heap);
 }

太好了,Jonathan,谢谢你! 那么,唯一有效使用堆的方法是浪费它吗? 也许OpenMP的某些实现有一个特殊的malloc函数,我需要研究一下。 顺便说一句,你关于关键块成为瓶颈的说法是不正确的。关键块在我的并行部分末尾,而不是for循环中。实际上,“约简”子句通过在并行部分末尾放置关键块来实现约简。但还是谢谢提醒! - drlemon
2
哦,但是(a)关键部分是一个非常耗费资源的操作,且(b)它比必要的粒度还要粗 - 你可以先做本地求和,然后只需进行关键操作(或更好的是,原子操作)来更新全局总和。但即使如此,使用大量线程仍然会更快一些,因为最终的归约可以按层次结构进行(在ln(线程数)的时间内完成,而不是在(线程数)的时间内完成)。 - Jonathan Dursi
3
关于有效利用堆内存--避免伪共享是所有共享内存操作通用的问题,唯一的解决方法是确保您拥有至少相隔一个缓存行的不重叠内存块。该间距的大小将取决于系统;将其设置为多个K太过严格,通常512字节左右足矣。 - Jonathan Dursi
当然,你说的没错,我可以对这段小代码进行一些微调。我的关键部分使用实际上是我正在解决的问题的产物。在那里,我有Fortran 90派生类型的数组,而不是整数数组,我无法想出更优雅的方法来为这些线程的单独结果求和。 - drlemon
2
为了其他观众的方便,这里提供一个查询缓存行大小的讨论链接:https://dev59.com/M3RA5IYBdhLWcg3w4SDo - drlemon

0

最初的问题暗示堆数组比栈数组慢。不幸的是,这种缓存行冲突的特定情况导致了这种缓慢,而且只在多线程应用程序中出现。这并不能证明堆数组总体上比栈数组慢。 对于大多数情况,性能没有显著差异,特别是当数组远大于缓存行大小时。相反,使用可分配的堆数组,针对所需的大小,可以带来比要求更多内存传输的较大固定大小数组更好的性能优势。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接