Paralelismo de bucles en OpenMP

Parámetros

Cláusula Parámetro
privado Lista separada por comas de variables privadas
primeroprivado Como private, pero inicializado al valor de la variable antes de entrar en el bucle
últimoprivado Como private, pero la variable obtendrá el valor correspondiente a la última iteración del ciclo al salir
reducción operador de reducción : lista separada por comas de las variables de reducción correspondientes
horario static, dynamic, guided, auto o runtime con un tamaño de fragmento opcional después de una coma para los 3 primeros
colapso Número de bucles perfectamente anidados para colapsar y paralelizar juntos
ordenado Indica que algunas partes del ciclo deberán mantenerse en orden (estas partes se identificarán específicamente con algunas cláusulas “ordenadas” dentro del cuerpo del ciclo)
sin esperar Eliminar la barrera implícita existente por defecto al final de la construcción del bucle

El significado de la cláusula schedule es el siguiente:

  • static[,chunk]: distribuye estáticamente (lo que significa que la distribución se realiza antes de entrar en el bucle) las iteraciones del bucle en lotes de tamaño chunk de forma rotativa. Si no se especifica chunk, entonces los fragmentos son lo más parejos posible y cada subproceso obtiene como máximo uno de ellos.
  • dynamic[,chunk]: distribuya las iteraciones de bucle entre los subprocesos por lotes de tamaño chunk con una política de orden de llegada, hasta que no quede ningún lote. Si no se especifica, chunk se establece en 1
  • guided[,chunk]: como dynamic pero con lotes cuyos tamaños son cada vez más pequeños, hasta 1
  • auto: Deje que el compilador y/o la biblioteca de tiempo de ejecución decidan qué es lo más adecuado
  • runtime: Aplazar la decisión en tiempo de ejecución mediante la variable de entorno OMP_SCHEDULE. Si en tiempo de ejecución la variable de entorno no está definida, se utilizará la programación predeterminada

El valor predeterminado para programación es definición de implementación. En muchos entornos es ’estático’, pero también puede ser ‘dinámico’ o muy bien podría ser ‘automático’. Por lo tanto, tenga cuidado de que su implementación no dependa implícitamente de él sin configurarlo explícitamente.

En los ejemplos anteriores, usamos la forma fusionada parallel for o parallel do. Sin embargo, la construcción loop se puede usar sin fusionarla con la directiva parallel, en la forma de una directiva independiente #pragma omp for [...] o !$omp do [...] dentro de un región ‘paralela’.

Solo para la versión de Fortran, las variables de índice de bucle de los bucles paralizados son siempre “privadas” de forma predeterminada. Por lo tanto, no hay necesidad de declararlos explícitamente privados (aunque hacerlo no es un error).
Para la versión C y C++, los índices de bucle son como cualquier otra variable. Por lo tanto, si su alcance se extiende fuera de los bucles paralelizados (es decir, si no se declaran como for (int i = ...) sino como int i; ... for (i = ...) ) entonces tienen que ser declarados privados.

Ejemplo típico en C

#include <stdio.h>
#include <math.h>
#include <omp.h>

#define N 1000000

int main() {
    double sum = 0;

    double tbegin = omp_get_wtime();
    #pragma omp parallel for reduction( +: sum )
    for ( int i = 0; i < N; i++ ) {
        sum += cos( i );
    }
    double wtime = omp_get_wtime() - tbegin;

    printf( "Computing %d cosines and summing them with %d threads took %fs\n",
            N, omp_get_max_threads(), wtime );

    return sum;
}

En este ejemplo, solo calculamos 1 millón de cosenos y sumamos sus valores en paralelo. También cronometramos la ejecución para ver si la paralelización tiene algún efecto en el rendimiento. Finalmente, dado que medimos el tiempo, debemos asegurarnos de que el compilador no optimice el trabajo que hemos realizado, por lo que pretendemos usar el resultado simplemente devolviéndolo.

Mismo ejemplo en Fortran

program typical_loop
    use omp_lib
    implicit none
    integer, parameter :: N = 1000000, kd = kind( 1.d0 )
    real( kind = kd ) :: sum, tbegin, wtime
    integer :: i

    sum = 0

    tbegin = omp_get_wtime()
    !$omp parallel do reduction( +: sum )
    do i = 1, N
        sum = sum + cos( 1.d0 * i )
    end do
    !$omp end parallel do
    wtime = omp_get_wtime() - tbegin

    print "( 'Computing ', i7, ' cosines and summing them with ', i2, &
        & ' threads took ', f6.4,'s' )", N, omp_get_max_threads(), wtime

    if ( sum > N ) then
        print *, "we only pretend using sum"
    end if
end program typical_loop

Aquí nuevamente calculamos y acumulamos 1 millón de cosenos. Temporizamos el bucle y, para evitar una optimización no deseada del compilador, pretendemos usar el resultado.

Compilando y ejecutando los ejemplos

En una máquina Linux de 8 núcleos con GCC versión 4.4, los códigos C se pueden compilar y ejecutar de la siguiente manera:

$ gcc -std=c99 -O3 -fopenmp loop.c -o loopc -lm
$ OMP_NUM_THREADS=1 ./loopc
Computing 1000000 cosines and summing them with 1 threads took 0.095832s
$ OMP_NUM_THREADS=2 ./loopc
Computing 1000000 cosines and summing them with 2 threads took 0.047637s
$ OMP_NUM_THREADS=4 ./loopc
Computing 1000000 cosines and summing them with 4 threads took 0.024498s
$ OMP_NUM_THREADS=8 ./loopc
Computing 1000000 cosines and summing them with 8 threads took 0.011785s

Para la versión de Fortran, da:

$ gfortran -O3 -fopenmp loop.f90 -o loopf
$ OMP_NUM_THREADS=1 ./loopf
Computing 1000000 cosines and summing them with  1 threads took 0.0915s
$ OMP_NUM_THREADS=2 ./loopf
Computing 1000000 cosines and summing them with  2 threads took 0.0472s
$ OMP_NUM_THREADS=4 ./loopf
Computing 1000000 cosines and summing them with  4 threads took 0.0236s
$ OMP_NUM_THREADS=8 ./loopf
Computing 1000000 cosines and summing them with  8 threads took 0.0118s

Adición de dos vectores usando OpenMP paralelo para construir

void parallelAddition (unsigned N, const double *A, const double *B, double *C)
{
    unsigned i;

    #pragma omp parallel for shared (A,B,C,N) private(i) schedule(static)
    for (i = 0; i < N; ++i)
    {
        C[i] = A[i] + B[i];
    }
}

Este ejemplo agrega dos vectores (A y B en C) generando un equipo de subprocesos (especificados por la variable de entorno OMP_NUM_THREADS, por ejemplo) y asignando a cada subproceso una parte del trabajo (en este ejemplo, asignado estáticamente a través de la expresión schedule(static)).

Ver la sección de comentarios con respecto a la opcionalidad private(i).