Soy un usuario de OpenMP bastante experimentado, pero acabo de encontrarme en un problema desconcertante, y tengo la esperanza de que alguien aquí pueda ayudar. El problema es que un algoritmo simple de hash funciona bien para las matrices asignadas a la pila, pero deficiente para las matrices en el montón.OpenMP: bajo rendimiento de las matrices de pila (las matrices de pila funcionan bien)
El siguiente ejemplo utiliza i% M (i módulo M) para contar cada M-ésimo entero en el elemento de matriz respectivo. Para simplificar, imagine N = 1000000, M = 10. Si N% M == 0, entonces el resultado debería ser que cada elemento de contenedores [] es igual a N/M:
#pragma omp for
for (int i=0; i<N; i++)
bins[ i%M ]++;
bins array [] es privada para cada hilo (resultados de suma I de todas las discusiones en una sección crítica después).
Cuando bins [] está asignado en la pila, el programa funciona muy bien, con una escala de rendimiento proporcionalmente al número de núcleos.
Sin embargo, si los contenedores [] están en el montón (el puntero a los contenedores [] está en la pila), el rendimiento disminuye drásticamente. ¡Y ese es un gran problema!
Quiero agrupar en paralelo (hash) ciertos datos en matrices de montón con OpenMP, y este es un golpe de rendimiento importante.
Definitivamente no es algo tonto como todos los hilos que intentan escribir en la misma área de memoria. Esto se debe a que cada subproceso tiene su propia matriz de bins [], los resultados son correctos con los bins asignados tanto en pila como en pila, y no hay diferencia en el rendimiento para ejecuciones de subproceso único. Reproduje el problema en diferentes hardware (Intel Xeon y AMD Opteron), con compiladores GCC e Intel C++. Todas las pruebas fueron en Linux (Ubuntu y RedHat).
No parece haber ninguna razón por la que el buen rendimiento de OpenMP deba limitarse a las matrices de pila.
¿Alguna conjetura? ¿Tal vez el acceso de los hilos al montón pasa a través de algún tipo de puerta de enlace compartida en Linux? ¿Cómo arreglo eso?
programa completo para jugar con es a continuación:
#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;
}
salidas de ejemplo del programa son a continuación:
para 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).
y para 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).
¡Agradecería cualquier ayuda!
Esto es genial, Jonathan, ¡gracias! Entonces, ¿significa que la única forma de usar el montón de manera eficiente es perdiéndolo? Quizás algunas implementaciones de OpenMP tengan una función especial de malloc, tendré que investigar. Por cierto, lo que dices acerca de que el bloque crítico es un cuello de botella es incorrecto. El bloque crítico está al final de mi sección paralela, y no dentro del ciclo for. De hecho, la cláusula 'reducción 'logra la reducción haciendo exactamente eso, colocando un bloque crítico al final de la sección paralela. Pero gracias por el cara a cara! – drlemon
Ah, pero (a) una operación crítica es muy pesada, y (b) es más gruesa de lo necesario; primero se puede hacer su suma local, luego hacer crítica (o mejor, una atómica) para actualizar la global suma. Pero aun así, con una gran cantidad de subprocesos, la reducción será aún más rápida, ya que la reducción final se puede realizar jerárquicamente (en ln (número de subprocesos) en vez de en (cantidad de subprocesos)) –
En cuanto a la eficiencia uso de heap: evitar el uso compartido falso es un problema que es genérico para todas las operaciones de memoria compartida, y la única forma de evitarlo es asegurarse de tener fragmentos de memoria disjuntos que estén separados al menos por una línea de caché. El tamaño de ese espaciado dependerá del sistema; por lo que es múltiple K fue excesivo, por lo general 512 bytes más o menos lo hará. –