Laço “for” paralelo em C
- Posted by acidx on November 1st, 2008 filed in geek
- Comment now »
A OpenMP é fantástica: poderosa, fácil de usar e há suporte para vários compiladores. Entretanto, nem sempre é possível (ou desejável) usá-la; eu não gosto de aumentar o número de dependências dos meus programas, portanto tento encontrar soluções “feitas em casa” quando possível. Reinventar a roda também é uma boa maneira para aprender como as coisas funcionam.
Uma das funcionalidades que os usuários do HardInfo mais pedem é a realização de benchmarks levando em consideração máquinas multiprocessadas. Somente depois de conhecer o modelo fork-join que a OpenMP usa, que eu consegui implementar isso no HardInfo. Para quem não conhece esse modelo, é bem simples: em pontos paralelizáveis de seu código, n linhas de execução são criadas (aqui, n equivale ao número de núcleos) — e a execução linear do programa só retoma após um ponto de encontro. A criação das threads é o “fork”, e o ponto de encontro é o “join”.
A OpenMP facilita a vida provendo alguns #pragmas: o compilador com suporte a ela, então, gera o código responsável pela criação das threads e pelo ponto de encontro. Um desses pragmas é o “parallel for“, que divide um laço ao estilo do for em n linhas distintas. Essa construção tem suas limitações — só deve ser usada em laços vanilla: aqueles que apenas contam de um número até outro (a linguagem C permite fazer praticamente qualquer coisa num for e isso não é suportado pela OpenMP).
Como os benchmarks do HardInfo são geralmente feitos dentro de um laço for, repetindo testes pequenos várias vezes e contando o tempo (o que causa alguns problemas de precisão, mas ainda não consegui encontrar maneira melhor), recriar essa construção do OpenMP foi a saída mais natural.
A recriação foi trivial: o modelo é simples e eu tenho como obter o número de núcleos tranquilamente (afinal, o HardInfo é um programa de informações de hardware). Como a implementação foi feita sem requerer suporte do compilador (apenas usando funções), há a necessidade da criação de uma função, e da substituição do laço por uma chamada de função.
Por exemplo, se houvesse o interesse em paralelizar o seguinte código:
for (i = 0; i < 1000; i++) {
vetor[i] = i * 3;
}
Eu criaria uma função:
void preenche_vetor(guint start, guint end, gpointer data)
{
int *vetor = (int *)data;
for (i = start; i < end; i++) {
vetor[i] = i * 3;
}
}
E substituiria o laço por uma chamada ao paralelizador de for:
parallel_for(preenche_vetor, 0, 1000, vetor);
A função parallel_for então determinaria o número de núcleos, criaria uma linha de execução para cada elas, e se encarregaria de esperar pelo término de todas elas. Como bônus, ela também retorna o tempo gasto desde a criação das threads até o término de todas elas — ignorados neste exemplo.
Paralelizei dois benchmarks no HardInfo: o de compressão/descompressão com a zlib, e o ray tracer. O ganho, em um Pentium Dual Core (E2160), foi da ordem de 1,86x mais rápido no caso da zlib, e 1,41x no caso do ray tracer.
O código está disponível no repositório git mestre do HardInfo, disponível na minha conta do github.












Leave a Comment