OpenMP: słaba wydajność tablic sterty (tablice stosu działają dobrze)

Jestem dość doświadczonym użytkownikiem OpenMP, ale właśnie napotkałem zagadkowy problem i mam nadzieję, że ktoś tutaj mógłby pomóc. Problem polega na tym, że prosty algorytm mieszający działa dobrze dla tablic przydzielanych stosem, ale słabo dla tablic na stercie.

Poniższy przykład używa i % m (i modulus M) do zliczania każdej M-tej liczby całkowitej w danym elemencie tablicy. Dla uproszczenia, wyobraź sobie N = 1000000, M = 10. Jeżeli N % M= = 0, to wynik powinien być taki, że każdy element Bina [] jest równy N / M:

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

Pojemniki tablicy [] są prywatne dla każdego wątku(sumuję wyniki wszystkich wątków w sekcji krytycznej).

Gdy pojemniki[] są przydzielane na stosie, program działa świetnie, ze skalowaniem wydajności proporcjonalnie do liczby rdzeni.

Jeśli jednak bins [] znajduje się na stosie (wskaźnik do bins[] znajduje się na stosie), wydajność drastycznie spada. I to jest poważny problem!

Chcę równoległe binowanie (hashowanie) pewnych danych do tablic sterty z OpenMP, a to jest wielki hit wydajności.

To zdecydowanie nie jest coś głupiego, jak wszystkie wątki próbujące zapisać w tym samym obszarze pamięci. Dzieje się tak dlatego, że każdy wątek ma własną tablicę bins [], wyniki są poprawne zarówno dla skrzynek przydzielonych stosem, jak i stosem, i nie ma różnicy w wydajności dla uruchomień pojedynczych wątków. Odtworzyłem problem na innym sprzęcie (Intel Xeon i AMD Opteron), z kompilatorami GCC i Intel C++. Wszystkie testy były na Linuksie (Ubuntu i RedHat).

Wydaje się, że nie ma powodu, dla którego dobra wydajność OpenMP powinna być ograniczona do tablic stosu.

Jakieś przypuszczenia? Może dostęp wątków do sterty przechodzi przez jakąś wspólną bramę w Linuksie? Jak to naprawić?

Kompletny program do zabawy znajduje się poniżej:

#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;
}

Przykładowe wyniki programu są poniżej:

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

I dla 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).
Będę bardzo wdzięczny za każdą pomoc!
Author: drlemon, 2011-07-07

2 answers

To jest ładny problem: z kodem jak wyżej (gcc4. 4, Intel i7) z 4 wątkami dostaję

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

Ale jeśli zmienię linię malloc na

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

(Update : or even

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

)

Potem dostaję

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

Problemem jest tutaj fałszywe dzielenie się. Domyślny malloc jest bardzo wydajny (space -) i umieszcza żądane małe alokacje w jednym bloku pamięci, obok siebie; ale ponieważ alokacje są tak małe, że wielokrotne dopasowanie w tej samej linii pamięci podręcznej, co oznacza, że za każdym razem, gdy jeden wątek aktualizuje swoje wartości, brudzi linię pamięci podręcznej wartości w sąsiednich wątkach. Jeśli żądana pamięć jest wystarczająco duża, nie stanowi to już problemu.

Nawiasem mówiąc, powinno być jasne, dlaczego przypadek przypisany do stosu nie widzi tego problemu; różne wątki-różne stosy-pamięć na tyle duża, że fałszywe dzielenie nie jest problemem.

Jako punkt poboczny -- to nie ma znaczenia dla M z rozmiar, którego tu używasz, ale jeśli Twoje m (lub liczba wątków) była większa, OMP krytyczne byłoby dużym seryjnym wąskim gardłem; możesz użyć redukcji OpenMP aby efektywniej zsumować sumę kontrolną

#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);
 }
 24
Author: Jonathan Dursi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-08 16:10:59

Pierwsze pytanie sugerowało, że tablice sterty są wolniejsze niż tablice stosu. Niestety przyczyna tej powolności związana jest ze szczególnym przypadkiem kolizji linii pamięci podręcznej w aplikacjach wielowątkowych. Nie uzasadnia to implikacji, że ogólnie tablice sterty są wolniejsze od tablic stosowych. W większości przypadków nie ma znaczącej różnicy w wydajności, zwłaszcza gdy tablice są znacznie większe niż rozmiar linii pamięci podręcznej. Często może być odwrotnie, ponieważ użycie allocable macierze sterty, dostosowane do wymaganego rozmiaru, mogą prowadzić do przewagi wydajnościowej nad większymi macierzami o stałym rozmiarze, które wymagają większych transferów pamięci.

 0
Author: johncampbell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-10 14:08:37