Executando verificação de segurança...
9

[CONTEÚDO] Como paralelizar seu código C/C++ com OpenMP

O que é o OpenMP?

OpenMP (Open Multi-Processing) é uma ferramenta de paralelização de código Fortran, C e C++.

Paralelizando seu código C++

Para paralelizar seu código C++ é muito simples, basta uma linha e conseguimos extrair o máximo que seu computador pode ter. Abaixo temos um exemplo disso, temos uma função que soma repetidamente o valor 1 a uma variável de redução.

std::size_t Soma(std::size_t numero_maximo_de_iteracoes) {

    std::size_t reducao = 0;
    for(std::size_t iter = 0; iter < numero_maximo_de_iteracoes; ++iter) {
        reducao += 1;
    }

    return reducao;
}

E para paralelizar o código? O que precisamos fazer? Bom, essa é a parte fácil, apenas com uma linha, como mostra o código abaixo, conseguimos fazer o loop for ser executado em paralelo.

#pragma omp parallel for reduction(+:reducao)

Adicionando essa diretiva de compilação antes do loop for da função Soma, dizermos ao compilador que queremos paralelizar esse loop.

std::size_t SomaParalela(std::size_t numero_maximo_de_iteracoes) {

    std::size_t reducao = 0;
    #pragma omp parallel for reduction(+:reducao)
    for(std::size_t iter = 0; iter < numero_maximo_de_iteracoes; ++iter) {
        reducao += 1;
    }

    return reducao;
}

Quando adicionamos o pragma omp parallel é criado uma nova região paralela, o OpenMP distribui a carga automaticamente entre as threads, nos testes a seguir coloquei para distribuir entre 4 threads a partir de uma variável ambiente chamada OMP_NUM_THREADS, você também consegue fazer essa manipulação da quantidade de variáveis dentro do programa, porem ela não pode ser maior que o valor de OMP_NUM_THREADS, por fim pedi para reduzir o valor dessa soma das threads na variável reducao com essa última parte da linha (reduction(+:reducao)).

Abaixo temos uma ilustração simplista de como está sendo feito o balanceamento de carga para cada thread e a redução.

ilustracao-openmp

Após criar uma região paralela com a diretiva pragma omp parallel é possível fazer uma melhor manipulação dos dados dentro dela, mas não irei abordar aqui pois é um tópico mais avançado.

Resultados e Tempo de execução das Somas

Obs.: Os resultados são variados de computador para computador, dependendo do número de threads o resultado pode ser bem diferente.

Para 1000000000 de iterações a versão paralela conseguiu executar essa soma em 485.8 milissegundos em contrapartida a versão normal da soma executou em 1715.6 milissegundos . A versão paralela foi 3.53 vezes mais rápido que a versão não paralelizada.

Porem note que a quantidade maxima de iterações está bastante alta, como será o resultado se baixarmos para apenas 1000 iterações? A soma não paralelizada foi finalizada em 0.001994 milissegundos e a versão paralelizada terminou em 0.168432 milissegundos, tendo como campeã para poucas iterações a versão não paralelizada da soma que foi 84 vezes mais rápido que a versão paralelizada.

E quando usar o OpenMP?

Para responder essa pergunta, trouxe outra função que faz um loop duplo e com 1000 iterações para cada loop já notamos uma diferença. Os códigos abaixo fazem uma subtração da soma do loop mais interno com o valor de redução total que está fora dos loops sendo assim o retorno da função tem que ser 0.

std::size_t LoopDuplo(std::size_t  maximo_de_iteracoes_loop1, std::size_t maximo_iteracoes_loop2) {

    std::size_t reducao_total = maximo_de_iteracoes_loop1*maximo_iteracoes_loop2;
    for(std::size_t iter1 = 0; iter1 < maximo_de_iteracoes_loop1; ++iter1) {
        std::size_t reducao = 0;
        for(std::size_t iter2 = 0; iter2 < maximo_iteracoes_loop2; ++iter2) {
            reducao += 1;
        }
        reducao_total -= reducao;
    }

    return reducao_total;
}
std::size_t LoopDuploParalelo(std::size_t  maximo_de_iteracoes_loop1, std::size_t maximo_iteracoes_loop2) {
    std::size_t reducao_total = maximo_de_iteracoes_loop1*maximo_iteracoes_loop2;

    #pragma omp parallel for reduction(-:reducao_total)
    for(std::size_t iter1 = 0; iter1 < maximo_de_iteracoes_loop1; ++iter1) {
        std::size_t reducao = 0;
        for(std::size_t iter2 = 0; iter2 < maximo_iteracoes_loop2; ++iter2) {
            reducao += 1;
        }
        reducao_total -= reducao;
    }

    return reducao_total;
}

Com 1000 iterações para cada um dos loops, conseguimos um ganho de 5 vezes para a função paralelizada.

E Onde mais podemos utilizar?

Podemos utilizar OpenMP para paralelizar o redimensionamento de fotos em lote, por exemplo, uma pasta contendo milhares de fotos e queremos reduzir seu tamanho em 25%, podemos utilizar paralelismo para essa tarefa, onde cada thread ficaria responsável por uma foto.

Também é muito utilizado em simulações químicas para encontrar novos medicamentos.

E como ultimo exemplo, podemos falar sobre o treinamento de IAs, temos o exemplo bastante recente ai do ChatGPT. Porem nesse caso é mais aconselhável a utilização de GPUs.

Obs.: Para coisas simples talvez não seja vantajoso paralelizar seu código. E a partir do OpenMP 4.0 é possível também fazer a paralelização do seu código direto para a GPU.

Boas Praticas Utilizando OpenMP

Sempre setar o número de threads igual ao número de cores do computador, por exemplo:

Se seu computador é um 6c/12t (ou seja, 6 cores físicos e 12 threads), coloque a variável ambiente OMP_NUM_THREADS igual a 6.

Se atentar a condições de corrida no código, o OpenMP fornece diretivas e também o uso de variáveis privadas (O OpenMP criara uma copia dessa variável para cada thread)

Resultados Tabelados

FunçãoNúmero de IteraçõesTempo(ms)Número de IteraçõesTempo(ms)
Soma10000.00199410000000001715.6
Soma Paralela10000.1684321000000000485.8
FunçãoNúmero de Iterações dos LoopsTempo(ms)Número de Iterações dos LoopsTempo(ms)
Loop Duplo10001.5110000122.18
Loop Duplo Paralelo10000.311000032.22

Repositório com o código dos testes: https://github.com/italonicacio/como-paralelizar-seu-codigo-c-cpp-com-openmp/tree/main/project

Carregando publicação patrocinada...
2

Parabéns pela iniciativa @italonicacio!

Só salientar que OpenMP não faz mágica, e assim como muitas formas de programar altamente abstraídas, ele vem com uma pegadinha: sobrecusto (overhead). Por baixo dos panos, o pré-processador gera código para criação e lançamento/gerenciamento das threads a serem usadas pelo OpenMP. Isso explica porque você não foi capaz de observar ganho de desempenho ao rodar um caso pequeno.

1

Muito obrigado!!!

Sabia que tinha um overhead mas não que tinha um impacto ai com poucas iterações, sei que paralelismo não é bom para poucas iterações, por exemplo, o tempo de instanciar novas threads as vezes é maior do que o tempo execução. Fiquei até curioso agora, se o overhead da biblioteca Threads é menor do que o do OpenMP.

E outra coisa tambem aos poucos estou tentando lançar mais conteudo aqui no Tabnews sobre desenvolvimento com C++ e HPC.