Paralelismo de loop no OpenMP

Parâmetros

Cláusula Parâmetro
privado Lista separada por vírgulas de variáveis ​​privadas
firstprivate Como private, mas inicializado com o valor da variável antes de entrar no loop
lastprivate Como private, mas a variável obterá o valor correspondente à última iteração do loop na saída
redução operador de redução : lista separada por vírgulas de variáveis ​​de redução correspondentes
horário static, dynamic, guided, auto ou runtime com um tamanho de bloco opcional após uma vírgula para os 3 primeiros
colapso Número de loops perfeitamente aninhados para recolher e paralelizar juntos
pedido Diz que algumas partes do loop precisarão ser mantidas em ordem (essas partes serão identificadas especificamente com algumas cláusulas ordenadas dentro do corpo do loop)
agora Remova a barreira implícita existente por padrão no final da construção do loop

O significado da cláusula schedule é o seguinte:

  • static[,chunk]: Distribua estaticamente (o que significa que a distribuição é feita antes de entrar no loop) as iterações do loop em lotes de tamanho chunk de forma round-robin. Se chunk não for especificado, então os pedaços são os mais uniformes possíveis e cada thread recebe no máximo um deles.
  • dynamic[,chunk]: Distribua as iterações de loop entre os encadeamentos por lotes de tamanho chunk com uma política de ordem de chegada, até que nenhum lote permaneça. Se não for especificado, chunk é definido como 1
  • guided[,chunk]: como dynamic, mas com lotes cujos tamanhos ficam cada vez menores, até 1
  • auto: Deixe o compilador e/ou a biblioteca de tempo de execução decidir o que é mais adequado
  • runtime: Adiar a decisão em tempo de execução por meio da variável de ambiente OMP_SCHEDULE. Se em tempo de execução a variável de ambiente não estiver definida, o agendamento padrão será usado

O padrão para schedule é definição de implementação. Em muitos ambientes é estático, mas também pode ser dinâmico ou pode muito bem ser automático. Portanto, tenha cuidado para que sua implementação não dependa implicitamente dela sem defini-la explicitamente.

Nos exemplos acima, usamos a forma fundida parallel for ou parallel do. No entanto, a construção do loop pode ser usada sem fundi-la com a diretiva parallel, na forma de uma diretiva autônoma #pragma omp for [...] ou !$omp do [...] dentro de um região paralela.

Somente para a versão Fortran, a(s) variável(is) de índice de loop(s) do(s) loop(s) paralisado(s) é(são) sempre privado por padrão. Portanto, não há necessidade de declará-los explicitamente como privados (embora isso não seja um erro).
Para a versão C e C++, os índices de loop são como quaisquer outras variáveis. Portanto, se seu escopo se estender para fora do(s) loop(s) paralelizado(s) (ou seja, se eles não forem declarados como for ( int i = ...) mas sim como int i; ... for ( i = ... ) então eles precisam ser declarados como privados.

Exemplo típico em 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;
}

Neste exemplo, apenas calculamos 1 milhão de cossenos e somamos seus valores em paralelo. Também cronometramos a execução para ver se a paralelização tem algum efeito no desempenho. Finalmente, como medimos o tempo, temos que ter certeza de que o compilador não otimizará o trabalho que fizemos, então fingimos usar o resultado apenas retornando-o.

Mesmo exemplo em 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

Aqui novamente calculamos e acumulamos 1 milhão de cossenos. Cronometramos o loop e, para evitar a otimização indesejada do compilador, fingimos usar o resultado.

Compilando e executando os exemplos

Em uma máquina Linux de 8 núcleos usando o GCC versão 4.4, os códigos C podem ser compilados e executados da seguinte maneira:

$ 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 a versão Fortran, fornece:

$ 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

Adição de dois vetores usando OpenMP paralelo para construção

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 exemplo adiciona dois vetores (A e B em C) gerando uma equipe de threads (especificada pela variável de ambiente OMP_NUM_THREADS, por exemplo) e atribuindo a cada thread um pedaço de trabalho (neste exemplo, atribuído estaticamente através da expressão schedule(static)).

Veja a seção de comentários com respeito à opcionalidade private(i).