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

drl*_*mon 20 heap performance stack multithreading openmp

我是一个相当有经验的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 ]++;
Run Code Online (Sandbox Code Playgroud)

数组bins []对每个线程都是私有的(我在之后对关键部分中所有线程的结果进行求和).

当在堆栈上分配bins []时,程序运行良好,性能与内核数量成比例缩放.

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

我希望使用OpenMP将某些数据的binning(散列)并行化为堆数组,这是一个重大的性能影响.

绝对不是像所有线程试图写入同一内​​存区域那样愚蠢的东西.这是因为每个线程都有自己的bins []数组,结果对于堆栈和堆栈分配的bin都是正确的,并且单线程运行的性能没有差别.我使用GCC和英特尔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;
}
Run Code Online (Sandbox Code Playgroud)

该计划的样本输出如下:

对于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).
Run Code Online (Sandbox Code Playgroud)

并且对于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).
Run Code Online (Sandbox Code Playgroud)

我非常感谢任何帮助!

Jon*_*rsi 24

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

但如果我将malloc线更改为

    int* bins_thread_heap=(int*)malloc(sizeof(int)*M*1024);
Run Code Online (Sandbox Code Playgroud)

(更新:甚至

    int* bins_thread_heap=(int*)malloc(sizeof(int)*16);
Run Code Online (Sandbox Code Playgroud)

)

然后我明白了

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).
Run Code Online (Sandbox Code Playgroud)

这里的问题是虚假分享.默认的malloc非常(空间)高效,并将请求的小分配全部放在一个内存块中,彼此相邻; 但由于分配太小以至于多个适合同一缓存行,这意味着每次一个线程更新其值时,它会污染相邻线程中值的缓存行.通过使请求的内存足够大,这不再是一个问题.

顺便说一句,应该清楚为什么堆栈分配的情况没有看到这个问题; 不同的线程 - 不同的堆栈 - 内存足够远,虚假共享不是问题.

作为一个侧面点 - 对于你在这里使用的大小的M来说并不重要,但是如果你的M(或线程数)更大,那么omp关键将是一个很大的连续瓶颈; 您可以使用OpenMP减少更有效地对校验和求和

#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);
 }
Run Code Online (Sandbox Code Playgroud)

  • 啊,但是(a)一个关键是一个非常重量级的操作,并且(b)它比必要的粗粒度 - 你可以先做你的局部和,然后做关键(或更好,一个原子)来更新全局总和.但即便如此,由于大量线程的减少仍然会更快,因为最终的减少可以分层次地完成(以ln(线程数)时间,而不是(线程数)时间.) (2认同)
  • 至于堆的有效使用 - 避免错误共享是一个对所有共享内存操作都是通用的问题,并且避免它的唯一方法是确保你拥有至少相隔高速缓存行的不相交的内存块.该间距的大小取决于系统; 使它多倍K是过度的,通常512字节左右就可以了. (2认同)
  • 对于其他观众而言,这里有一个关于查询缓存行大小的讨论的链接http://stackoverflow.com/questions/794632/programmatically-get-the-cache-line-size (2认同)