Módulo: OpenMP

Logo UFPR

OpenMP

Logo Licenciatura

1. Introdução ao OpenMP

OpenMP é uma API que simplifica o paralelismo em C, C++ e Fortran. Ela utiliza diretivas (pragmas) no código-fonte para criar e gerenciar threads de forma prática, sem a necessidade de manipular threads manualmente.

Compilação:

gcc -fopenmp exemplo.c -o exemplo

O parâmetro -fopenmp habilita o suporte ao OpenMP no compilador GCC.

Alterando a quantidade de threads:

export OMP_NUM_THREADS=3

O comando export OMP_NUM_THREADS altera a quantidade de threads.

Execução com contagem de tempo:

time ./exemplo

O comando time mede o tempo total de execução do programa no sistema.

Exemplo básico:

#include stdio.h
#include omp.h

int main() {
    omp_set_num_threads(4); // Define o número de threads a serem usadas
    double inicio = omp_get_wtime();

    #pragma omp parallel
    {
        int id = omp_get_thread_num();
        printf("Olá da thread %d\n", id);
    }

    double fim = omp_get_wtime();
    printf("Tempo interno: %f segundos\n", fim - inicio);
    return 0;
}

Esse programa demonstra os conceitos básicos do OpenMP:

A saída mostra "Olá da thread X" para cada thread ativa e o tempo total de execução do bloco paralelo.


Exemplo adicional: Medição de desempenho

#include stdio.h
#include omp.h
#include math.h

int main() {
    int n = 10000000;
    double sum = 0.0;
    
    // Serial
    double start_serial = omp_get_wtime();
    for (int i = 0; i < n; i++) {
        sum += sin(i) * cos(i);
    }
    double end_serial = omp_get_wtime();
    
    // Parallel
    double sum_parallel = 0.0;
    double start_parallel = omp_get_wtime();
    #pragma omp parallel for reduction(+:sum_parallel)
    for (int i = 0; i < n; i++) {
        sum_parallel += sin(i) * cos(i);
    }
    double end_parallel = omp_get_wtime();
    
    printf("Serial:   %f sec, Result: %f\n", end_serial - start_serial, sum);
    printf("Parallel: %f sec, Result: %f\n", end_parallel - start_parallel, sum_parallel);
    printf("Speedup: %.2fx\n", (end_serial-start_serial)/(end_parallel-start_parallel));
    
    return 0;
}

Este exemplo mostra como medir o ganho de desempenho (speedup) ao paralelizar um cálculo matemático intensivo.

2. Parallel For

A diretiva #pragma omp parallel for é uma das mais poderosas do OpenMP. Ela combina a criação de um bloco paralelo (#pragma omp parallel) com a distribuição automática das iterações de um loop entre as threads disponíveis. Isso reduz a complexidade de gerenciar paralelismo em loops.

Como funciona:

Exemplo básico:

#include stdio.h
#include omp.h

int main() {
    omp_set_num_threads(4); // Define 4 threads para o exemplo
    int i;

    #pragma omp parallel for
    for (i = 0; i < 10; i++) {
        printf("Thread %d executando iteração %d\n", omp_get_thread_num(), i);
    }
    return 0;
}

Neste exemplo, as 10 iterações (0 a 9) são divididas entre 4 threads. Por exemplo, a thread 0 pode executar as iterações 0-2, a thread 1 as iterações 3-5, e assim por diante, dependendo da estratégia de escalonamento do OpenMP.

Exemplo prático com cálculo:

#include 
#include 

int main() {
    int n = 1000000; // Um milhão de elementos
    double soma = 0.0;
    omp_set_num_threads(4);

    double inicio = omp_get_wtime();
    #pragma omp parallel for reduction(+:soma)
    for (int i = 0; i < n; i++) {
        soma += 1.0 / (i + 1); // Soma harmônica parcial
    }
    double fim = omp_get_wtime();

    printf("Soma: %.10f\n", soma);
    printf("Tempo: %f segundos\n", fim - inicio);
    return 0;
}

Este exemplo calcula uma soma harmônica parcial em paralelo:

Variações úteis:

O schedule controla como as iterações são alocadas, permitindo otimização conforme a natureza do problema.


Exemplo adicional: Escalonamento dinâmico

#include stdio.h
#include omp.h
#include unistd.h

void heavy_work(int i) {
    usleep(i * 1000); // Simula trabalho variável
}

int main() {
    int n = 20;
    
    printf("Static schedule:\n");
    #pragma omp parallel for schedule(static)
    for (int i = 0; i < n; i++) {
        printf("Thread %d processing %d\n", omp_get_thread_num(), i);
        heavy_work(i);
    }
    
    printf("\nDynamic schedule:\n");
    #pragma omp parallel for schedule(dynamic, 1)
    for (int i = 0; i < n; i++) {
        printf("Thread %d processing %d\n", omp_get_thread_num(), i);
        heavy_work(i);
    }
    
    return 0;
}

Este exemplo demonstra a diferença entre escalonamento estático e dinâmico, útil quando as iterações têm cargas de trabalho desbalanceadas.

3. Sections e Tasks

A diretiva #pragma omp sections divide blocos de código entre threads, enquanto #pragma omp task cria tarefas independentes.

Exemplo com sections:

#pragma omp parallel sections
{
    #pragma omp section
    printf("Seção 1 por thread %d\n", omp_get_thread_num());
    #pragma omp section
    printf("Seção 2 por thread %d\n", omp_get_thread_num());
}

Exemplo avançado com tasks

#include stdio.h
#include omp.h
#include stdlib.h

int fib(int n) {
    if (n < 2) return n;
    
    int x, y;
    #pragma omp task shared(x)
    x = fib(n-1);
    #pragma omp task shared(y)
    y = fib(n-2);
    #pragma omp taskwait
    return x + y;
}

int main() {
    int n = 10;
    double start = omp_get_wtime();
    
    #pragma omp parallel
    {
        #pragma omp single
        {
            printf("Fib(%d) = %d\n", n, fib(n));
        }
    }
    
    double end = omp_get_wtime();
    printf("Time: %f seconds\n", end - start);
    return 0;
}

Este exemplo implementa recursivamente a sequência de Fibonacci usando tasks, demonstrando como paralelizar algoritmos recursivos.

4. Shared e Private

No OpenMP, o controle de variáveis é essencial para garantir o funcionamento correto do paralelismo. As variáveis podem ser classificadas como:

Comportamento padrão:

Por padrão, variáveis declaradas fora de um bloco paralelo são shared, enquanto variáveis locais dentro do bloco (como o índice de um loop) precisam ser explicitamente declaradas como private para evitar conflitos.

Exemplo básico:

#include stdio.h
#include omp.h

int main() {
    int i;
    omp_set_num_threads(4);

    #pragma omp parallel for private(i)
    for (i = 0; i < 5; i++) {
        printf("Thread %d, valor de i: %d\n", omp_get_thread_num(), i);
    }
    return 0;
}

Neste exemplo, i é declarado como private. Cada thread tem sua própria cópia de i, que é usada para controlar as iterações que ela executa. Sem o private(i), haveria uma condição de corrida, pois todas as threads tentariam modificar a mesma variável i.

Exemplo com shared e private:

#include stdio.h
#include omp.h

int main() {
    int contador = 0; // Variável compartilhada
    omp_set_num_threads(4);

    #pragma omp parallel private(contador_privado)
    {
        int contador_privado = omp_get_thread_num(); // Variável privada
        contador_privado += 1; // Cada thread modifica sua cópia

        #pragma omp critical
        contador += 1; // Acesso seguro à variável compartilhada

        printf("Thread %d: contador_privado = %d, contador compartilhado = %d\n",
               omp_get_thread_num(), contador_privado, contador);
    }
    return 0;
}

Explicação:


Exemplo prático com array:

#include stdio.h
#include omp.h

int main() {
    int n = 10;
    int array[10];
    omp_set_num_threads(4);

    #pragma omp parallel for private(i)
    for (int i = 0; i < n; i++) {
        array[i] = i * omp_get_thread_num(); // Cada thread escreve no array
        printf("Thread %d escreveu array[%d] = %d\n", omp_get_thread_num(), i, array[i]);
    }

    printf("Resultado final do array: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
    return 0;
}

Neste caso:

Cuidados:


Exemplo avançado: firstprivate e lastprivate

#include stdio.h
#include omp.h

int main() {
    int x = 10, y = 20, z = 30;
    
    printf("Before parallel: x=%d, y=%d, z=%d\n", x, y, z);
    
    #pragma omp parallel for firstprivate(x) lastprivate(y) private(z)
    for (int i = 0; i < 4; i++) {
        printf("Thread %d: x=%d, y=%d, z=%d\n", 
               omp_get_thread_num(), x, y, z);
        x += i;  // Modifica cópia local
        y = i;   // Será sobrescrito pela última iteração
        z = 100; // Modifica cópia local
    }
    
    printf("After parallel: x=%d, y=%d, z=%d\n", x, y, z);
    return 0;
}

Este exemplo demonstra:

5. Critical e Atomic

No OpenMP, quando múltiplas threads acessam ou modificam uma variável compartilhada simultaneamente, podem ocorrer condições de corrida, resultando em resultados inconsistentes. Para evitar isso, usamos:

Diferenças principais:

Exemplo básico com critical:

#include 
#include 

int main() {
    int soma = 0;
    omp_set_num_threads(4);

    #pragma omp parallel for
    for (int i = 0; i < 100; i++) {
        #pragma omp critical
        soma += i; // Apenas uma thread por vez atualiza soma
    }
    printf("Soma: %d (esperado: 4950)\n", soma);
    return 0;
}

Neste exemplo, sem o critical, as threads poderiam sobrescrever as atualizações umas das outras em soma. Com critical, a soma de 0 a 99 (4950) é calculada corretamente, mas a execução é mais lenta devido à exclusão mútua.

Exemplo com atomic:

#include stdio.h
#include omp.h

int main() {
    int soma = 0;
    omp_set_num_threads(4);

    double inicio = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < 100; i++) {
        #pragma omp atomic
        soma += i; // Operação atômica, mais eficiente
    }
    double fim = omp_get_wtime();

    printf("Soma: %d (esperado: 4950)\n", soma);
    printf("Tempo: %f segundos\n", fim - inicio);
    return 0;
}

Aqui, atomic substitui critical. Como a operação é um simples incremento, atomic é mais rápido, pois evita a sincronização completa de um bloco crítico.

Exemplo prático com critical e múltiplas operações:

#include stdio.h
#include omp.h

int main() {
    int soma = 0, maior = 0;
    omp_set_num_threads(4);

    #pragma omp parallel for
    for (int i = 0; i < 100; i++) {
        #pragma omp critical
        {
            soma += i;          // Soma acumulada
            if (i > maior)      // Atualiza maior valor
                maior = i;
        }
    }
    printf("Soma: %d, Maior: %d\n", soma, maior);
    return 0;
}

Neste caso, usamos critical porque temos múltiplas operações (soma e comparação) que dependem de variáveis compartilhadas. O atomic não seria suficiente aqui, pois só protege uma única operação.

Boas práticas:


Exemplo com critical nomeado:

#pragma omp parallel
{
    #pragma omp critical(soma_crit)
    soma += 1;
    #pragma omp critical(print_crit)
    printf("Thread %d\n", omp_get_thread_num());
}

Seções críticas nomeadas permitem que diferentes blocos sejam sincronizados independentemente.


Exemplo comparativo: critical vs atomic vs reduction

#include stdio.h
#include omp.h
#include time.h

#define N 1000000

int main() {
    int sum_critical = 0, sum_atomic = 0, sum_reduction = 0;
    
    // Método 1: Critical
    double start = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        #pragma omp critical
        sum_critical += i;
    }
    double end_critical = omp_get_wtime();
    
    // Método 2: Atomic
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        #pragma omp atomic
        sum_atomic += i;
    }
    double end_atomic = omp_get_wtime();
    
    // Método 3: Reduction
    #pragma omp parallel for reduction(+:sum_reduction)
    for (int i = 0; i < N; i++) {
        sum_reduction += i;
    }
    double end_reduction = omp_get_wtime();
    
    printf("Critical:  %f sec\n", end_critical - start);
    printf("Atomic:    %f sec\n", end_atomic - end_critical);
    printf("Reduction: %f sec\n", end_reduction - end_atomic);
    
    return 0;
}

Este exemplo compara três métodos para realizar uma soma em paralelo, mostrando que reduction é geralmente o mais eficiente.

Quiz

1. Qual diretiva inicia um bloco paralelo no OpenMP?

2. O que a diretiva #pragma omp parallel for faz?

3. Qual é a diferença entre #pragma omp sections e #pragma omp task?

4. O que acontece se uma variável não for declarada como private em um loop paralelo?

5. Quando devemos preferir #pragma omp atomic em vez de #pragma omp critical?

Perguntas

1. Qual a diferença entre #pragma omp parallel for e #pragma omp for?

O #pragma omp parallel for combina a criação de um bloco paralelo com a divisão das iterações de um loop entre threads, enquanto #pragma omp for apenas divide as iterações entre threads já existentes em um bloco paralelo iniciado por #pragma omp parallel. Em resumo, parallel for é um atalho para paralelismo e divisão, enquanto for exige um contexto paralelo prévio.

2. Como o OpenMP decide quantas threads usar se omp_set_num_threads não for chamado?

Se omp_set_num_threads não for especificado, o OpenMP usa o número de núcleos disponíveis no processador como padrão. Alternativamente, o número de threads pode ser definido pela variável de ambiente OMP_NUM_THREADS (ex.: export OMP_NUM_THREADS=4), que tem prioridade sobre o padrão do sistema se configurada.

3. Qual é a vantagem de usar #pragma omp sections em vez de executar blocos de código sequencialmente?

A diretiva #pragma omp sections permite que blocos de código independentes sejam executados em paralelo por diferentes threads, reduzindo o tempo total de execução em comparação com uma execução sequencial. Cada #pragma omp section dentro de sections é atribuído a uma thread, aproveitando o paralelismo para tarefas distintas que não dependem umas das outras.

4. Por que declarar uma variável como private é importante em um loop paralelo?

Declarar uma variável como private em um loop paralelo (ex.: #pragma omp parallel for private(i)) é crucial para evitar condições de corrida. Sem isso, a variável seria shared por padrão, permitindo que todas as threads a modificassem simultaneamente, o que poderia levar a resultados imprevisíveis. Com private, cada thread tem sua própria cópia, garantindo independência e consistência.

5. Em que situações #pragma omp critical é preferível a #pragma omp atomic?

O #pragma omp critical é preferível quando múltiplas operações ou um bloco de código complexo precisam ser protegidos contra acesso concorrente (ex.: atualizar duas variáveis compartilhadas ou executar uma lógica condicional). Já o #pragma omp atomic é limitado a operações simples, como incrementos ou atribuições, sendo mais eficiente nesses casos. Use critical para flexibilidade e atomic para desempenho em tarefas básicas.

Referências