OpenMP: API para escribir aplicaciones SMP portátiles Fernando Félix-Redondo Universidad Carlos III Departamento de Informática
Índice Introducción: OpenMP a vista de pájaro Sintaxis básica: Empezando con OpenMP “Constructores” de OpenMP: Regiones paralelas Trabajo compartido (Worksharing) Entorno de datos Sincronización Funciones en tiempo de ejecución y variables de entorno
Evitando errores de programación SMP Compiladores OpenMP
OpenMP a vista de pájaro
Para programar sistemas con memoria distribuida:
Hay que usar “bibliotecas de funciones” para paso de mensajes como MPI o PVM
Para los sistemas de memoria compartida:
Es posible también usar paso de mensajes pero …
Existen soluciones basadas directamente en el paradigma de memoria compartida. Ejemplo: OpenMP
Shared Memory
OpenMP a vista de pájaro
OpenMP es un API para escribir aplicaciones multihilos (multithreaded)
Conjunto de directivas de compilación, algunas funciones de biblioteca y unas pocas variables de entorno
OpenMP es básicamente lo mismo en C/C++ y en Fortran
OpenMP
Directivas de compilación
Funciones de Librería en tiempo de ejecución
Variables de Entorno
#pragma omp parallel for omp_get_thread_num() OMP_NUM_THREADS
OpenMP a vista de pájaro
Modelo de programación denominado “Paralelismo Fork-Join”
El hilo maestro arranca un conjunto de hilos cuando se necesita paralelizar una determinada tarea
Al terminar su labor, los hilos creados terminan y continua la ejecución secuencial
Generalmente, el paralelismo se añade de forma incremental a un programa secuencial pre-existente
OpenMP a vista de pájaro
OpenMP se usa habitualmente para paralelizar bucles
Se localizan los bucles que consumen más tiempo
Se reparten entre los distintos hilos o threads
double V[10000]; for (int i=0; i< 10000; i++) { calculo_brutal( V[i] ); }
Hay que evitar las condiciones de carrera entre hilos
La compartición de datos de forma no intencionada puede dar problemas
Se puede usar sincronización para evitar conflictos de datos
La sincronización es “cara” por lo que…
…hay que distribuir los datos entre los hilos con inteligencia
double V[10000]; #pragma omp parallel for for (int i=0; i< 10000; i++) { calculo_brutal( V[i] ); }
Índice Introducción: OpenMP a vista de pájaro Sintaxis básica: Empezando con OpenMP “Constructores” de OpenMP: Regiones paralelas Trabajo compartido (Worksharing) Entorno de datos Sincronización Funciones en tiempo de ejecución y variables de entorno
Evitando errores de programación SMP Compiladores OpenMP
Sintaxis básica: Empezando con OpenMP
Como primer paso hay que incluir el fichero de cabeceras al principio del programa en C/C++: #include
#include using namespace std; int main(int argc, char* argv[]) { omp_set_num_threads(10); // Número de Hilos #pragma omp parallel // Ejecuta en paralelo a partir de aquí cout << "Hello World!!" << endl; // Aquí de nuevo es secuencial la ejecución return 0; }
Sintaxis básica: Constructores
Los “constructores” de OpenMP son básicamente directivas de compilación (pragmas) #pragma omp construct [clause [clause]…]
Un compilador NO-OpenMP simplemente ignorará las “pragmas” Con OpenMP, es posible que el mismo código fuente sirva para la versión secuencial y paralela de un programa
Sintaxis básica: Compilación condicional #include #include using namespace std; int main(int argc, char* argv[]) { int nthreads=1; #pragma omp parallel // Ejecuta en paralelo a partir de aquí { cout << "Hello World!!" << endl;
Si se usa OpenMP, estará definida
#ifdef _OPENMP nthreads=omp_get_num_threads(); // Averiguar el número de Hilos
#endif cout << "Somos " << nthreads << " threads" << endl; } // Aquí de nuevo es secuencial la ejecución return 0; }
Sintaxis básica: Bloques estructurados
La mayoría de los constructores OpenMP se aplican sobre bloques estructurados Bloque estructurado: Bloque de código con un único punto de entrada al principio y un único punto de salida al final; la única excepción es exit()
double V[10000], Z[10000];
#pragma omp parallel for for (int i=0; i< 10000; i++) { calculo_brutal( V[i] ); Z[i]=func(i)*sqrt(V[i] }
double V[10000], Z[10000];
#pragma omp parallel for for (int i=0; i< 10000; i++) { calculo_brutal( V[i] ); if (cond(V[i])) goto salida; Z[i]=func(i)*sqrt(V[i] }
Índice Introducción: OpenMP a vista de pájaro Sintaxis básica: Empezando con OpenMP “Constructores” de OpenMP: Regiones paralelas Trabajo compartido (Worksharing) Entorno de datos Sincronización Funciones en tiempo de ejecución y variables de entorno
Evitando errores de programación SMP Compiladores OpenMP
Constructores OpenMP
Tenemos 5 categorías de constructores OpenMP:
Regiones paralelas
Trabajo compartido (worksharing)
Entorno de datos
Sincronización
Funciones “runtime” y variables de entorno
Regiones paralelas
La expresión “regiones paralelas” se podría traducir, simplemente, como “hilos” (threads)
Este constructor permite crear un conjunto de hilos (threads) en OpenMP double V[10000]; omp_set_num_threads(4);
Cada thread ejecutará el código perteneciente al bloque y llama a “hazalgo” con un ID diferente
#pragma omp parallel { int ID =omp_thread_num(); hazalgo(ID,V);
} printf(“done”);
Regiones paralelas
double V[10000]; omp_set_num_threads(4);
#pragma omp parallel {
double V[10000]
int ID =omp_thread_num(); boo(ID,V);
omp_set_num_threads(4)
hazalgo(0,V)
hazalgo(1,V)
} printf(“done”);
hazalgo(2,V)
hazalgo(3,V)
printf(“done”)
Los threads esperan aquí a que todos hayan terminado (sinc. barrera)
Una sola copia de V es compartida por todos los hilos
Regiones paralelas: Algunos detalles
Modo dinámico (por defecto): El
número de hilos puede cambiar de una región paralela a otra
Fijar
el número de hilos, solo establece el número máximo de hilos posible – podrían obtenerse menos
Modo estático: El
número de hilos es fijo y controlado por el programador
OpenMP permite anidar regiones paralelas, pero… El
compilador puede decidir serializar la región anidada
Trabajo compartido (Worksharing)
El constructor “for” permite dividir las iteraciones de un bucle entre diferentes hilos
Por defecto hay una barrera al final del “omp for” Se puede usar la cláusula “nowait” para desactivarla double V[10000];
#pragma omp parallel #pragma omp for for (int i=0; i< 10000; i++) { calculo( V[i] ); }
Trabajo compartido: Ejemplo ilustrativo for (i=0;i
Trabajo compartido: Cláusula schedule
Esta cláusula permite controlar como repartimos las iteraciones del bucle entre los threads
schedule(static [,#n])
schedule(dynamic [,#n])
Cada hilo toma “#n” iteraciones de una cola hasta que todas se han acabado.
schedule(guided[,#n])
Reparte bloques de iteraciones de tamaño “#n” a cada hilo.
Los hilos dinámicamente toman bloques de iteraciones. El tamaño del bloque al principio es grande y disminuye hasta “#n” a medida que progresa la ejecución.
schedule(runtime)
La política schedule y el tamaño de bloque se toma de la variable de entorno OMP_SCHEDULE.
(Ejemplo: export OMP_SCHEDULE=“dynamic,10”)
Trabajo compartido: cláusula if
Si las iteraciones no suponen “suficiente trabajo” no tiene sentido ejecutarlas en paralelo Interesa que la decisión sobre si se paraleliza o no se tome en tiempo de ejecución Con “if” el bucle se ejecutará en paralelo si la condición “cond” se evalúa como VERDAD Si no, el bucle se ejecuta secuencialmente, sin incurrir en ninguna penalización por overhead.
double V[10000]; #pragma omp parallel for \ if (N> 1000) for (int i=0; i< N; i++) { calculo(V,i); }
El compilador incluye por sí mismo el código necesario para implementar la sentencia condicional
Trabajo compartido: Anidamiento de bucles
Cuando uno de los bucles del anidamiento tiene una cláusula parallel for, ésta solo aplica al que le sigue inmediatamente int N=1000; int M=1000;
int N=1000; int M=1000;
#pragma omp parallel for for (int i=0; i< N; i++) {
for (int i=0; i< N; i++) { // Some operations here
// Some operations here for (int j=0;j<M;j++) { // Additional stuff } }
#pragma omp parallel for for (int j=0;j<M;j++) { // Additional stuff } }
Restricciones a los bucles paralelos
OpenMP establece algunas restricciones sobre los bucles que pueden ser paralelizados
Debe ser posible conocer el número de iteraciones en tiempo de ejecución antes de comenzarlo
El índice debe ser un integer
El incremento debe ser siempre igual
No se puede usar break o goto para salir del bucle; sí es posible usar goto dentro del propio bucle
Es posible usar continue
Si se generan excepciones en C++, éstas deben ser tratadas dentro del bloque del bucle
Trabajo compartido: constructor sections
Permite paralelizar una secuencia de tareas sin dependencias de datos entre ellas Asignamos cada tarea a un hilo diferente Es útil cuando resulta difícil paralelizar cada una de las tareas individualmente
#pragma omp parallel sections { X_calculo(); #pragma omp section Y_calculo(); #pragma omp section Z_calculo(); } Barrera implícita
{
X_calculo
Y_calculo
Z_calculo
} Barrera impícita
Trabajo compartido: constructor single
En ocasiones las regiones paralelas contienen tareas que deberían ser ejecutadas por un único hilo Un ejemplo típico lo constituye la entrada/salida dentro de una región paralela que debe ejecutarse secuencialmente El constructor single también incluye una barrera al final del bloque (a no ser que se use nowait)
#pragma omp parallel { #pragma omp single { leer_datos(X); }
Barrera implícita
calcular(X); calcular_mas() #pragma omp single nowait { escribir_resultado(X); } }
Aquí no hay barrera
Entorno de datos
Modelo de programación de Memoria Compartida
Las variables globales son compartidas entre los threads
La mayoría de las variables están compartidas por defecto
En C: Variables estáticas (static) o globales cuyo ámbito es el fichero
Pero no todo está compartido…
Las variables de pila en subprogramas llamados desde las regiones paralelas son PRIVADAS Las variables automáticas dentro de un bloque son PRIVADAS
Controlar la compartición de datos
Es fundamental que los hilos puedan también usar variables privadas. OpenMP tiene cláusulas para controlar el ámbito de los datos dentro de los constructores paralelos int N=1000; shared
int M=1000;
private
#pragma omp parallel for for (int i=0; i< N; i++) {
firstprivate
y lastprivate
a=a+10;
default
for (int j=0;j<M;j++) { // Additional stuff }
reduction }
Entorno de datos: cláusula shared
Establece que las variables especificadas serán compartidas por todos los hilos.
Sólo existe una instancia de cada una de esas variables
Cualquier modificación por parte de uno de los hilos, actualizará la instancia compartida.
Cualquier cambio sobre la variable de uno de los hilos es visto por los demás
Entorno de datos: cláusula private
Crea un copia “privada” de la variable para cada hilo La
variable “privada” no es inicializada y tiene un valor indefinido a la entrada y a la salida int N=1000; int a=1; #pragma omp parallel for private (a) for (int i=0; i< N; i++) { a=a+10; } cout << “El valor de a” << a << endl;
Entorno de datos: Por defecto…
Todas las variables usadas dentro de un constructor paralelo son tratadas como “compartidas” Es
el comportamiento deseado generalmente para variables de “sólo lectura” dentro del bucle paralelo
Si
son modificadas, hay que declararlas privadas o sincronizar el acceso a las mismas
Entorno de datos: Por defecto…(II) Los índices de los bucles paralelos son identificados como tal y declarados privados de forma automática por el compilador; pero…
…los índices de los bucles secuenciales dentro de secciones paralelas en C/C++ siguen siendo compartidos (shared).
Ejemplos
Aquí i es declarada automáticamente como variable privada por el compilador
int N=1000; int i,a=1; int x[1000]; #pragma omp parallel for for (i=0; i< N; i++) { x[i]=i+10; }
Ejemplo 1
int i,a=1,N=1000; Esta i, sin embargo, es compartida por todos los hilos. En este caso tendríamos problemas
#pragma omp parallel { int x[1000]; for (i=0; i< N; i++) { x[i]=i+10; } }
Ejemplo 2
Entorno de datos: reduction
La reducción de datos es un tipo de cálculo muy frecuente
Consiste en aplicar repetidamente un operador binario (+,-,x,…) sobre una determinada variable El compilador y el entorno de ejecución implementan este cálculo de la forma más eficiente posible Se usa la cláusula: reduction (oper : lista_var)
Operadores para cláusula reduction +
SUMA
-
RESTA
*
PRODUCTO
int N=1000; int a=1; #pragma omp parallel for reduction ( + : a) for (i=0; i< N; i++) a=+10; cout << “El valor de a es: “ << a << endl;
Sincronización
OpenMP tiene los siguientes constructores para soportar operaciones de sincronización:
critical section
atomic
Cada thread espera a que todos los threads lleguen
flush
Es un caso especial del constructor anterior que se usa para ciertas sentencias simples
barrier
Sólo un thread a la vez puede entrar en una sección crítica
Supone un punto en la ejecución donde un thread intenta crear un visión consistente de la memoria
ordered
Fuerza a que el bloque se ejecute en orden secuencial
Funciones runtime y variables de entorno
Rutinas para manejar locks
Rutinas para modificar/comprobar el número de hilos
omp_set_nested(), omp_set_dynamic() omp_get_nested, omp_get_dynamic()
¿Estoy en una región paralela?
omp_set_num_threads(), omp_get_num_threads() omp_get_thread_num(), omp_get_max_threads()
Encender/Apagar anidamiento y modo dinámico
omp_init_lock(), omp_set_lock() omp_unset_lock(), omp_test_lock()
omp_in_parallel()
¿Cuántos procesadores hay en el sistema?
omp_num_procs()
Funciones runtime y variables de entorno Variable
OMP_SCHEDULE OMP_NUM_THREADS OMP_DYNAMIC OMP_NESTED
Ejemplo
Descripción
“dynamic,4”
Especifica el tipo de planificación para los bucles paralelos
16
Especifica el número de hilos a usar durante la ejecución
TRUE o FALSE
Habilita/deshabilita el ajuste dinámico de hilos
TRUE o FALSE
Habilita/deshabilita el paralelismo anidado
Variables de entorno
Índice Introducción: OpenMP a vista de pájaro Sintaxis básica: Empezando con OpenMP “Constructores” de OpenMP: Regiones paralelas Trabajo compartido (Worksharing) Entorno de datos Sincronización Funciones en tiempo de ejecución y variables de entorno
Evitando errores de programación SMP Compiladores OpenMP
Índice Introducción: OpenMP a vista de pájaro Sintaxis básica: Empezando con OpenMP “Constructores” de OpenMP: Regiones paralelas Trabajo compartido (Worksharing) Entorno de datos Sincronización Funciones en tiempo de ejecución y variables de entorno
Evitando errores de programación SMP Compiladores OpenMP
Compiladores OpenMP
Intel C/C++ compiler for Linux Visual Studio 2005 y posteriores GCC versión 4.x SunStudio 12.x
Más información sobre OpenMP
Bibliografía
Chandra et al.; Parallel programming in OpenMP; Morgan-Kaufman Chapman et al.; Using OpenMP; MIT Press Quinn, M.; Parallel Programming in C with MPI and OpenMP; McGraw-Hill
Internet •
Matsson, T & Eigenmann, R; “OpenMP: An API for writing portable SMP application software”; Tutorial at Supercomputing ‘99