Open Multi-Processing es la API (biblioteca que define solo la interfaz) que aporta paralelismo multihilo en sistemas de memoria compartida en C/C++.
OpenMP se basa en el modelo fork-join, paradigma que proviene de los sistemas Unix, donde una tarea muy pesada se divide en X hilos (fork) con menor peso, para luego "recolectar" sus resultados al final y unirlos en un solo resultado (join).
La sintaxis básica disponible para informar al compilador dónde optimizar código es la directiva de OpenMP:
#pragma omp <directivas>
Toda directiva OpenMP a incluir, implica una sincronización obligatoria en todo el bloque. Es decir, el bloque de código se marcará como paralelo y se lanzarán hilos según las características de la directiva. Al final habrá una barrera para la sincronización de los diferentes hilos (salvo que implícitamente indique lo contrario):
-
parallel: Indica que la parte del código que la comprende es ejecutada por varios hilos. Si el número de iteraciones no es suficientemente grande, el gasto de gestionar cada hilo, puede hacer que no haya ganancia. Algunas funciones de especial interés al hacer uso de varios hilos de ejecución:
- omp_get_max_threads: Ayuda a conocer el número máximo de hilos que lanzará nuestro programa en las zonas paralelas (útil para reservar memoria para cada hilo).
- omp_get_num_threads: Devuelve el número de hilos en ejecución.
- omp_get_num_procs: Devuelve el número de procesadores de nuestro ordenador disponibles.
-
section: Optimizar secciones para su ejecución por hilos diferentes. Hace que varias secciones de código se ejecuten de forma simultánea.
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
a = b + c;
#pragma omp section
d = e + f;
}
}
- for: Optimizar bucles for para su ejecución en varios hilos. Por defecto, todas las variables que trabajan en la región paralela (antes de informar al compilador de la presencia de OpenMP) son compartidas, excepto el iterador 'i' del bucle que es privado. La unión de la clausulas 'parallel' y 'for' permite la creación de hebras a la vez de especificar la estructura paralela.
#pragma omp parallel for
for (i=0; i < 1000; i++) {
a = b + c;
}
-
shared: Variable compartida por todos los hilos a paralelizar, lo cual significa que es accesible desde todos los threads.
-
private: Variable privada para cada hilo, que actúa como copia local. Una variable privada no es inicializada y tampoco es accesible desde fuera de la región paralela.
- firstprivate: Permite inicializar una variable privada con el valor original (entrada).
- lastprivate: Copia el último valor de una variable privada al objeto original (salida).
- threadprivate: Especifica que las variables se replican, teniendo cada hebra una copia. Como resultado, preserva el valor original de la variable en memoria.
-
critical: El uso inadecuado de variables compartidas origina las frecuentes condiciones de carrera. Esta directiva facilita que solo un hilo pueda estar en esta sección crítica y garantiza un acceso seguro a memoria compartida (disminuyen la eficiencia al reducir el paralelismo).
-
atomic: Asegura que una única posición de memoria debe ser modificada de forma atómica, sin permitir que múltiples threads escriban en esa posición de forma simultánea.
-
Otra alternativa es el uso de semáforos simples o anidados con: omp_init_lock, omp_destroy_lock, omp_set_lock, omp_unset_lock, ...
-
ordered: Obliga a ejecutar el código de manera paralela, pero en el mismo orden que hubiera sido ejecutado de manera secuencial. Suele aparecer sólo una vez en el contexto de una directiva for.
-
master: El código afectado sólo se ejecuta en el hilo maestro. El resto de threads saltan esa sección del código.
-
single: La parte del código que define esta directiva se ejecuta en un único hilo de todos los lanzados (no tiene porque ser en el maestro).
-
flush: Especifica un punto de sincronización donde todos los hilos tengan una visión consistente de cada variable de memoria. Está implícita en la mayoría de directivas, tanto de entrada (barrier) como de salida (nowait). Esa cláusula nowait invalida la barrera de sincronización para continuar con la siguiente sentencia del código sin esperar al resto.
-
reduction: La variable tiene una copia local en cada thread, cuyo valor final a asignar a memoria es obtenido de una operación con el resultado de cada hilo. Conviene usar la directiva para calcular, por ejemplo, el producto escalar de 2 vectores.
#pragma omp for reduction(+:sum)
for (i=0; i < length; i++) {
sum = sum + (a[i] * b[i])
}
- schedule: Al paralelizar un ciclo el número de iteraciones se divide por igual entre cada thread. Sin embargo, esto no es eficiente si las iteraciones tardan tiempos diferentes en ejecutarse. Esta directiva ayuda a reajustar la distribución de iteraciones de forma dinámica. Los tipos de scheduling son:
- static: Las iteraciones son divididas en partes de tamaño indicado, y las partes se asignan a las hebras mediante round-robin en el orden del numero de hebra.
- dynamic: Cada hebra ejecuta una parte de las iteraciones, a continuación pide otro parte hasta que no quedan trozos para su distribución.
- guided: Cada hebra ejecuta una parte de las iteraciones, a continuación pide otro parte hasta que no quedan trozos para su distribución. Las partes comienzan siendo grandes y se van reduciendo con forme a lo planificado.
- auto: La decisión de la planificación del reparto es delegada en el compilador o el sistema.