OpenMP: низкая производительность массивов кучи (массивы стека работают нормально) - PullRequest
20 голосов
/ 07 июля 2011

Я довольно опытный пользователь OpenMP, но я только что столкнулся с загадочной проблемой, и я надеюсь, что кто-то здесь может помочь.Проблема в том, что простой алгоритм хеширования хорошо работает для массивов, выделенных стеком, но плохо для массивов в куче.

В приведенном ниже примере i i M (i модуль M) подсчитывается для каждого M-го целого числа в соответствующихэлемент массива.Для простоты представим N = 1000000, M = 10.Если N% M == 0, то результатом должно быть то, что каждый элемент bin [] равен N / M:

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

Массив bin [] является частным для каждого потока (я суммирую результатывсе темы в критическом разделе впоследствии).

Когда бины [] размещены в стеке, программа работает отлично, с масштабированием производительности пропорционально количеству ядер.

Однако, если бины [] находятся в куче (указатель наbin [] в стеке), производительность резко падает.И это серьезная проблема!

Я хочу распараллелить объединение (хэширование) определенных данных в массивы кучи с помощью OpenMP, и это является серьезным ударом по производительности.

Это определенно не так глупо, каквсе потоки пытаются записать в одну область памяти.Это связано с тем, что каждый поток имеет свой собственный массив bin [], результаты корректны для двоичных объектов, выделенных как для кучи, так и для стека, и нет различий в производительности для однопоточных запусков.Я воспроизвел проблему на другом оборудовании (Intel Xeon и AMD Opteron) с компиляторами GCC и Intel C ++.Все тесты проводились в 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=10241024;
  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;
}

Ниже приведены примеры выходных данных программы:

*1024* для 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 ]

24 голосов
/ 07 июля 2011

Это милая проблема: с кодом, как указано выше (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 сокращения для более эффективного суммирования контрольной суммы

#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);
 }
0 голосов
/ 10 мая 2018

Первоначальный вопрос подразумевал, что массивы кучи работают медленнее, чем массивы стеков. К сожалению, причина такой медлительности связана с частным случаем столкновения строк кэша в многопоточных приложениях. Это не оправдывает предположение, что в целом массивы кучи работают медленнее, чем массивы стеков. В большинстве случаев нет существенной разницы в производительности, особенно когда массивы намного больше, чем размер строки кэша. Часто бывает наоборот, так как использование выделяемых массивов кучи, нацеленных на требуемый размер, может привести к преимуществам производительности по сравнению с массивами фиксированного размера с большими размерами, которые требуют большего объема памяти.

...