Nada como um projeto novo para entusiasmar no aprendizado de tecnologias diferentes. Já fazia algum tempo que eu estava com vontade de começar a trabalhar com programação paralela, mas sem algo concreto para dar o impulso inicial estava complicado.
Relembrando o algorítimo de cálculo do valor de pi com base em uma série de somas:
soma = 0 para cada i, enquanto i < LIMITE: soma += 4/(i*4 + 1) soma -= 4/(i*4 + 3) i += 1 fim para
Resolvi implementar esse pequeno código para testar uma primeira implementação usando o OpenMP. Na minha pesquisa de paralelismo de simulação de dinâmica dos Fluidos (CFD) vou usar a linguagem Fortran, por ser uma linguagem diretamente voltado à cálculos matemáticos. Porém, com uma rápida pesquisa sobre OpenMP, descobri que ela é suportada também pela linguagem C++, uma velha amiga, que eu sempre gostei muito de usar. Pensei então “por que não estudar como usar o OpenMP em ambas?”. Então, na medida do possível, pretendo repetir todos os experimentos e exemplos nas duas linguagens.
Como discutido no post anterior, a idéia do OpenMP é ser uma API que faz o gerenciamento dos Threads, distribuindo-os entre os núcleos/processadores disponíveis no sistema. Esse gerenciamento faz com que o programador não precise se preocupar com o sincronismo entre os threads, nem com a escalabilidade dos sistema.
Porém, o OpenMP é uma API, ou seja, uma ferramenta. E uma ferramenta só faz o que o programador a instrui fazer. Basicamente fica ao encargo do programador definir quais regiões serão paralelizadas, quais serão executadas de forma serial e quais são as variáveis compartilhadas e privadas de cada thread. O programador faz uso de algumas diretivas que são incluídas no código. Essas diretivas são avaliadas pelo pré-processador do OpenMP, e este faz o trabalho de dividir as tarefas entre os núcleos/processadores.
Tomemos como base a implementação em linguagem C do programa de cálculo do PI:
#include
#define NUM_PASSOS 20000000
int main(){
double pi = 0;
int i;
for(i = 0; i < NUM_PASSOS; i++){
pi += 4.0 / (4.0*i + 1.0);
pi -= 4.0 / (4.0*i + 3.0);
}
printf("-> %f\n",pi);
return 0;
}
O mesmo exemplo pode ser visto em Fortran aqui
Compilando esse código com o GCC, sem nenhuma flag de otimização e executando o código em um pc com processador intel ia32 com GNU/Linux o programa precisou de quase 1 segundo para executar:
[john@vaio rascunhos]$ time ./a.out -> 3.141593 real 0m0.906s user 0m0.870s sys 0m0.000s
Apenas para efeito de comparação, o código escrito em Fortran, compilado com o compilador ifort da intel, precisou de apenas 0.010s para executar. Um ganho de quase 90% que o Fortran proporciona, mas isso é assunto para um outro post.
O motivo pelo o qual esse código consumiu tanto tempo para ser executado está no número de repetições dentro do laço. A priori, em todo laço existe uma grande possibilidade de paralelizar o processo, dividindo as tarefas internas do laço entre os vário núcleos do meu sistema multiprocessado. Neste exemplo simples, percebam, a complexidade do meu algoritmo é linear, e relativa ao número de iterações. Em caso de complexidades mais críticas (quadrática, por exemplo), a paralelização do código traz benefícios ainda mais evidentes.
Devido a problemas estruturais (minha máquina não é multi-core) realizei o mesmo experimento com o auxílio do meu amigo Maluta em um Mac Mini que rendeu o seguinte resultado (não paralelizado, apenas para servir de base comparativa para o código paralelizado):
$ time ./a -> 3.141593 real 0m0.686s user 0m0.634s sys 0m0.002s
Para paralelizar o código devemos indicar, por meio de diretivas, quais trechos o compilador deve utilizar a API OpenMP para dividir em threads. A primeira diretiva apresentada é a diretiva OMP PARALLEL. Essa diretiva indica qual parte do código vai ser dividida (fork). O final de um trecho paralelo é sinalizado com um OMP END PARALLEL:
... ... trecho serial ... OMP PARALLEL ... ... trecho paralelizado ... OMP END PARALLEL ... ... serial novamente
Desta maneira, tudo que está no trecho paralelizado vai ser executado simultaneamente por todos os threads do sistema. Devemos salientar que as diretivas sofrem leves variações do C++ para o Fortran. Essas diferenças serão notadas nos códigos, mas a semântica é a mesma para ambos os casos.
Outra diretiva, senão a mais usada, é a que indica que um loop deve ser paralelizado. A diretiva OMP PARALLEL FOR ou OMP DO são usadas para indicar que um laço deve ser dividido. Como o intuito desse blog não é ser um tutorial de OpenMP ou uma referência formal, mais detalhes sobre a sintaxe de cada diretiva pode ser encontrada no site oficial do projeteo OpenMP.
Paralelizando o laço principal (e único) do meu programa, temos o seguinte código:
#include
#include
#define NUM_PASSOS 20000000
<pre>int main(){
double pi = 0;
int i;
#pragma omp parallel for
for(i = 0; i < NUM_PASSOS; i++){
pi += 4.0 / (4.0*i + 1.0);
pi -= 4.0 / (4.0*i + 3.0);
}
printf("-> %f\n",pi);
return 0;
}
O equivalente em Fortran pode ser encontrado aqui.
Para compilar o código usando o GCC e o OpenMP, é necessário usar a flg -fopenmp:
gcc -fopenmp <source> [-o nome]
O resultado da execução deste código foi:
$ time ./b -> 2.897633 real 0m0.544s user 0m1.041s sys 0m0.005s
Bem, o tempo de execução para o código linear foi de o.686s e no paralelo foi de 0.544s, ou seja, um ganho de aproximadamente 20%, porém, o meu resultado foi PI = 2.897633 ????????????? Alguma coisa não funcionou corretamente neste modelo de paralelismo.
A resposta é simples: quando o código foi paralelizada, cada parte da execução foi feita em um núcleo diferente. Assim, ao se fazer o join, as duas partes não são somadas, como deveriam. Em outras palavras, as variáveis Soma nesse caso estão separadas, e não compartilham o mesmo dado. O que devemos fazer nesse caso é forçar a união desses valores ao fim da execução dos threads. Para isso usamos mais uma diretiva, a diretiva REDUCTION:
#include
#include
#define NUM_PASSOS 20000000
int main(){
double pi = 0;
int i;
#pragma omp parallel for reduction (+:pi)
for(i = 0; i < NUM_PASSOS; i++){
pi += 4.0 / (4.0*i + 1.0);
pi -= 4.0 / (4.0*i + 3.0);
}
printf("-> %f\n",pi);
return 0;
}
Novamente, o resultado em Fortran se encontra aqui.
Compilando e executando o código, temos a seguinte saída:
$ time ./a -> 3.141593 real 0m0.433s user 0m0.634s sys 0m0.003s
Agora o nosso resultado confere com o valor de PI e o ganho em desempenho é de aproximadamente 36% comparado com o seu equivalente serial.
Com isso já temos base suficiente para brincar um tanto bom com paralelismo, e testar em outros algoritimos. Vale novamente ressaltar que esse blog não tem o intuito de ser um tutorial de OpenMP, estou apenas mostrandop por onde caminhei, e indicando onde encontrei as referências para estudo.

05/05/2010 às 14:13
Muito bom