OpenMP:堆数组的性能不佳(堆栈数组工作正常)

Posted

技术标签:

【中文标题】OpenMP:堆数组的性能不佳(堆栈数组工作正常)【英文标题】:OpenMP: poor performance of heap arrays (stack arrays work fine) 【发布时间】:2011-09-30 03:29: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 ]++;

Array bins[] 对每个线程都是私有的(之后我将所有线程的结果汇总到一个关键部分)。

在堆栈上分配 bins[] 时,程序运行良好,性能与内核数量成正比。

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

我想使用 OpenMP 将某些数据并行化(散列)到堆数组中,这对性能造成重大影响。

这绝对不像所有线程都试图写入同一内​​存区域那样愚蠢。 这是因为每个线程都有自己的 bins[] 数组,堆和堆栈分配的 bin 的结果都是正确的,并且单线程运行的性能没有差异。 我使用 GCC 和 Intel C++ 编译器在不同的硬件(Intel Xeon 和 AMD Opteron)上重现了这个问题。所有测试均在 Linux(Ubuntu 和 RedHat)上进行。

似乎没有理由将 OpenMP 的良好性能仅限于堆栈数组。

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

完整的程序如下:

#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).

非常感谢任何帮助!

【问题讨论】:

【参考方案1】:

这是一个可爱的问题:使用上面的代码(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).

这里的问题是false sharing。默认 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);
 

【讨论】:

这太好了,乔纳森,谢谢!那么这是否意味着有效使用堆的唯一方法就是浪费它?也许 OpenMP 的一些实现有一个特殊的 malloc 函数,我将不得不研究。顺便说一句,您所说的关键块是瓶颈是不正确的。关键块在我的并行部分的末尾,而不是在 for 循环内。事实上,“reduction”子句正是通过这样做来实现reduction,即在并行部分的末尾放置一个关键块。但是感谢您的提醒! 啊,但是(a)关键是一个非常重量级的操作,并且(b)它比必要的粒度更粗 - 您可以先进行本地求和,然后再进行关键(或更好,一个原子)来更新全局总和。但即便如此,如果有大量线程,减少仍然会更快,因为最终减少可以分层完成(在 ln(线程数)时间内,而不是在(线程数)时间内)。 关于堆的有效使用——避免错误共享是所有共享内存操作的通用问题,避免它的唯一方法是确保您有不相交的内存块至少相隔一个缓存行。该间距的大小将取决于系统;将其设为多个 K 是多余的,通常 512 字节左右就可以了。 当然,你对我可以为这个小代码做的小调整是正确的。我对临界区的使用实际上是我正在解决的实际问题的产物。在那里,我有 Fortran 90 派生类型的数组而不是整数数组,我终其一生都无法找到一种更优雅的方法来为这些数组求​​和各个线程的结果。 为了其他观众的利益,这里有一个关于查询缓存行大小的讨论链接***.com/questions/794632/…【参考方案2】:

最初的问题暗示堆数组比堆栈数组慢。不幸的是,这种缓慢的原因与多线程应用程序中缓存行冲突的特定情况有关。它不能证明通常堆数组比堆栈数组慢的暗示是合理的。 在大多数情况下,性能没有显着差异,尤其是在阵列远大于高速缓存行大小的情况下。情况往往相反,因为使用可分配堆数组,以所需大小为目标,与需要更多内存传输的较大固定大小数组相比,可以带来性能优势。

【讨论】:

以上是关于OpenMP:堆数组的性能不佳(堆栈数组工作正常)的主要内容,如果未能解决你的问题,请参考以下文章

如何提高 OpenMP 代码的性能?

C 数组实例化 - 堆栈或堆分配?

如何在 C++ 中创建一个位于堆而不是堆栈的数组?

openmp 共享数组

在java中初始化数组时的堆栈和堆内存[重复]

NSFetchedResultsController 性能不佳