miniaturadeimagen|Logo de OpenMP. OpenMP es una interfaz de programación de aplicaciones (API) para la programación multiproceso de memoria compartida en múltiples plataformas. Permite añadir concurrencia a los programas escritos en C, C++ y Fortran sobre la base del modelo de ejecución fork-join. Está disponible en muchas arquitecturas, incluidas las plataformas de Unix y de Microsoft Windows. Se compone de un conjunto de directivas de compilador, rutinas de biblioteca, y variables de entorno que influyen el comportamiento en tiempo de ejecución.

Por lo anterior, OpenMP puede verse como un modelo de programación paralela para memoria compartida y memoria distribuida en multiprocesadores. Este modelo de programación se basa en tener la disposición de varios hilos de ejecución concurrentes que nos permitan acceder a variables alojadas en zonas de la memoria a las que tienen acceso cada uno de ellos y en directivas de compilación; facilitando la programación al evitar el intercambio explícito de datos entre los diferentes procesadores.

Definido conjuntamente por proveedores de hardware y de software, OpenMP es un modelo de programación portable y escalable que proporciona a los programadores una interfaz simple y flexible para el desarrollo de aplicaciones paralelas, para plataformas que van desde las computadoras de escritorio hasta supercomputadoras. Una aplicación construida con un modelo de programación paralela híbrido se puede ejecutar en un cluster de computadoras utilizando OpenMP y MPI, o a través de las extensiones de OpenMP para los sistemas de memoria distribuida.

Historia En la primavera de 1997 Intel forma la OpenMP Architecture Review Board (OpenMP ARB) para el desarrollo de estructuras de repetición en paralelo con el lenguaje Fortran. Para octubre se lanza la versión 1.0 de la especificación OpenMP para Fortran y para el año siguiente empiezan a aparecer las primeras aplicaciones híbridas con MPI y OpenMP.

En 2001 se forma la cOMPunity creada por usuarios de OpenMP, así mismo se crean talleres en América del Norte, Europa y Asia.

En 2002 se comienza la fusión de las especificaciones de Fortran y C/C++ en la versión 2.0.

En 2005 se unifica el uso de C, C++ y Fortran en la versión 2.5, así mismo se realizó el primer taller internacional sobre OpenMP.

En 2008 se incorpora el paralelismo de tareas en la versión 3.0, un problema difícil ya que OpenMP se esfuerza por mantener su naturaleza de subprocesos mientras que se adapta a la naturaleza dinámica de las tareas.

En 2013 se lanza la versión 4.0 la cual admite paralelismo SIMD de dispositivos aceleradores y/o coprocesadores, afinidad de subprocesos y mucho más. Provocando que OpenMP opere más allá de sus límites tradicionales.

En 2015 se lanza la versión 4.5 la cual añade soporte para algunas características de Fortran 2003.

En 2018 se lanza la versión 5.0 la cual ya tuvo dos reediciones que son la 5.1 lanzada en 2020 y la versión más actual la 5.2 lanzada en 2021.

Se ha visto que los principales lanzamientos de OpenMP han aparecido cada 5 años desde la primera versión, con algunas otras versiones menores lanzadas en el medio de este periodo de tiempo. En cualquiera de los dos casos, cuando se publica una versión, siempre hay un período de tiempo antes de que los desarrolladores del compilador se pongan al día para agregar todas las nuevas funciones y correcciones a su lenguaje. Esto se puede notar mucho en las versiones más recientes, las versiones 5.1 y 5.2. Debido a que la compatibilidad con el compilador no está completamente implementada para la versión 5.0 de OpenMP. OpenMP ARB (es decir, la corporación propietaria de OpenMP) ha calificado la versión 5.0 como un "gran avance", ya que en sus propias palabras: "agrega muchas características nuevas que serán útiles para aplicaciones altamente paralelas y complejas", muchas de las cuales fueron solicitadas por los usuarios. Con esto, se espera que una próxima versión 6.0 contenga más soporte para sistemas en tiempo real y algunos cambios en la terminología.

Modelo de ejecución OpenMP se basa en el modelo fork-join, paradigma que proviene de los sistemas Unix, donde una tarea muy pesada se divide en K hilos (fork) con menor peso, para luego "recolectar" sus resultados al final y unirlos en un solo resultado (join). El modelo de programación paralela en memoria compartida se basa básicamente en disponer de varios hilos de ejecución concurrentes que pueden acceder a variables alojadas en zonas de la memoria a las que tienen acceso todos ellos. Cada uno de estos hilos tendrá un identificador que le otorgará la posibilidad de tomar decisiones, incluyendo la coordinación de estos para realizar diferentes acciones. Esto facilita mucho la programación, ya que se evita tener que intercambiar explícitamente datos entre los diferentes procesadores.

OpenMP es un sistema de programación de memoria compartida basado en directivas e compilación, es decir, el programador inserta directivas en el código secuencial para indicar al compilador cómo ha de realizar la paralelización, además de incorporar algunas llamadas a funciones específicas. Cuando se incluye una directiva de compilador OpenMP esto implica que se incluye 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 que nos dé la directiva, y al final de ella habrá una barrera para la sincronización de los diferentes hilos (salvo que implícitamente se indique lo contrario con la directiva nowait). Este tipo de ejecución se denomina fork-join.

OpenMP también soporta el modelo de paralelismo de tareas. El equipo de hilos del bloque de código paralelo ejecuta las tareas especificadas dentro de dicho bloque. Las tareas permiten un paralelismo asíncrono. Desde la versión 4.0 lanzada en 2013 admite la especificación de dependencias entre tareas, relegando a la biblioteca de tiempo de ejecución de OpenMP el trabajo de planificar las tareas y ponerlas en ejecución. Los hilos de ejecución irán ejecutando las tareas a medida que éstas estén disponibles (sus dependencias ya estén satisfechas). El uso de tareas da lugar a sincronización con una granularidad más fina. El uso de barreras no es estrictamente necesario, de manera que los hilos pueden continuar ejecutando tareas disponibles sin necesidad de esperar a que todo el equipo de hilos acabe un bloque paralelo. El uso de tareas con dependencias crea un grafo, pudiéndose aplicar propiedades de grafos a la hora de escoger tareas para su ejecución.

Salvo uso de implementaciones hardware de la biblioteca de tiempo de ejecución OpenMP (p.ej. en FPGAs), los sobrecostes de las tareas es mayor. Este sobrecoste ha de ser amortizado mediante el potencial paralelismo adicional que las tareas exponen.

Sintaxis básica La sintaxis básica que nos encontramos en una directiva de OpenMP es para C/C++: # pragma omp <directiva> [cláusula [ , ...] ...] para Fortran: !$OMP PARALLEL <directiva> [cláusulas] [ bloque de código ] !$OMP END <directiva>

A continuación se presentan algunas de las directivas disponibles y cláusulas aplicables a las directivas que ofrece OpenMP.

Directivas También se suelen llamar constructores: parallel: Esta directiva nos indica que la parte de código que la comprende puede ser ejecutada por varios hilos. Se crea una tarea implícita para cada hilo perteneciente al equipo de hilos creado por el parallel. En este fragmento de código se trata de explotar el potencial del paralelismo en una tarea de una serie geométrica que tiende al infinito para la ecuación:

/* Juan Carlos Lanuza Lazo MGA-NI 27 / Apr / 2017

Ejemplo de sumatoria paralela de una serie Armonica */ #include <omp.h> #include <stdio.h>

#define LIMIT 1000000000000

int main(void) { double serie = 0; long long i; #pragma omp parallel

#pragma omp for reduction(+:serie) for(i = 1;i <= LIMIT;i++) serie += 1.0/i;

printf("Valor de la Serie: %.5f\n",serie); return 0; } for: El equipo de hilos que se encuentra con el for ejecuta una o más fracciones de iteraciones como resultado de dividir el bucle delimitado por la directiva entre los hilos del equipo (el tamaño de cada partición dependerá de las cláusulas opcionales añadidas al for). Su formato es: #pragma omp parallel for [cláusula, ... , cláusula]. Cada fracción de iteraciones es una tarea implícita (el programador no ha particionado el bucle manualmente y anotado una tarea por cada trozo). section y sections: Indica secciones que pueden ejecutarse en paralelo pero por un único hilo. single: La parte de código que define esta directiva, solo se puede ejecutar un único hilo de todos los lanzados, y no tiene que ser obligatoriamente el hilo padre. master: La parte de código definida, solo se puede ejecutar por el hilo padre. critical: Solo un hilo puede estar en esta sección. Definen secciones críticas o condiciones de carrera. atomic: Se utiliza cuando la operación atañe a sólo una posición de memoria, y tiene que ser actualizada solo por un hilo simultáneamente. Operaciones tipo x++ o --x son las que usan esta cláusula. flush: Esta directiva resuelve la consistencia, al exportar a todos los hilos un valor modificado de una variable que ha realizado otro hilo en el procesamiento paralelo. barrier: Crea una barrera explícita en la que los hilos se quedan esperando hasta que todo el equipo la haya alcanzado. task: Permite la ejecución asíncrona mediante tareas explícitas. El bloque de código dentro de la directiva task es ejecutado por un único hilo, y la tarea es instanciada por el hilo que se encuentra con la directiva. Por ello es recomendable utilizar esta directiva dentro de una directiva single, evitando crear (y ejecutar) la misma tarea tantas veces como hilos haya. Su formato es: #pragma omp task [cláusulas]. Cuando la tarea se instancia ésta es insertada en una cola de pendientes. Las tareas de la cola de pendientes son seleccionadas por la implementación de OpenMP en función de sus dependencias cumplidas y prioridad. Las tareas seleccionadas pasan a la cola de listas para ejecutar. Los hilos del equipo activo cogen tareas de dicha cola y las ejecutan. Uso de task con single: #define NUM_ITERS 1000000 #pragma omp parallel { int32_t BS = NUM_ITERS/omp_get_num_threads(); #pragma omp single

for (int i = 0; i < NUM_ITERS; i+= BS) { int32_t upper = i+((NUM_ITERS-BS >= 0) ? BS : NUM_ITERS-BS); #pragma omp task firstprivate(i,upper)

for (int ii = i; ii < upper; ii++) a[ii] += b[ii]*c[ii];

#pragma omp taskwait

taskyield: Permite a una tarea ceder el control del núcleo del procesador a otra tarea. taskwait: Similar a barrier. El hilo instanciador de tareas se queda esperando en el taskwait hasta que todas las tareas previas al taskwait hayan terminado su ejecución. No sirve para esperar a las tareas anidadas, que requerirán un taskwait anidado. Todos los hilos, sean los que sean, mientras permanecen en una barrera, sea ésta implícita o explícita, comprueban la cola de tareas listas para ejecutar. Si hay tareas pendientes, las ejecutarán.

Cláusulas de visibilidad de datos shared(valor-i_1, ..., valor-i_N): Los datos de la región paralela son compartidos, lo que significa que son visibles y accesibles por todos los hilos. Por definición, todas las variables que trabajan en la región paralela son compartidas excepto el contador de iteraciones. private(valor-i_1, ..., valor-i_N): Los datos de la región paralela nombrados por private son copiados al área de almacenamiento local del hilo (principalmente su pila), lo que significa que cada hilo los usará como variable temporal. Una variable privada no es inicializada y tampoco se mantiene fuera de la región paralela. Por definición, el contador de iteraciones en OpenMP es privado.