Paralelização de Aplicações com OpenMP

Introdução

O OpenMP é uma norma/API para programação paralela em sistemas de memória partilhada para as linguagens de programação C, C++ e Fortran, desenvolvida e mantida pelo OpenMP Architecture Review Board. Disponibiliza uma alternativa simples e portável a soluções de mais baixo nível como POSIX Threads, e é suportado por vários compiladores como o GCC ou ICC, e deverá chegar em breve ao Clang/LLVM.

O OpenMP distingue-se de outras soluções para suporte a paralelismo em memória partilhada pelo seu nível de abstracção, na medida em que o essencial das suas funcionalidades é obtido através de um conjunto de directivas do compilador que especificam de forma declarativa como é que diferentes partes do código podem ser executadas em paralelo. Adicionalmente, um compilador que não suporte OpenMP pode simplesmente ignorar estas directivas, continuando a aplicação a funcionar correctamente (embora de forma sequencial). Contudo, aplicações mais complexas podem também necessitar de chamadas a funções de mais baixo nível (disponibilizados pela API do OpenMP), e que tornam a compilação da aplicação dependente do OpenMP.

Neste artigo pretende-se mostrar como podem explorar paralelismo em memória partilhada recorrendo a um pequeno conjunto de funcionalidades do OpenMP, de modo a tirarem partido dos processadores com múltiplos núcleos disponíveis na generalidade dos PCs hoje em dia. Utilizar-se-á o C com linguagem de suporte neste artigo. Os programas serão compilados usando o GCC 4.7.2.

Conceitos Básicos

O OpenMP usa threads para obter paralelismo. Todas as threads usam o mesmo espaço de endereçamento de memória, sendo que os dados podem ser partilhados por todas as threads, ou privados de cada thread (cada thread tem uma cópia da variável à qual só ela acede). De notar que o OpenMP utiliza um modelo de memória de consistência relaxada, na medida em que cada threads pode ter uma cópia local temporária (por exemplo, em registos ou cache) cujo valor não necessita de ser constantemente consistente com o valor global visto por todas as outras threads (vários mecanismos são disponibilizados para que o programador possa forçar a consistência de memória em determinados pontos da aplicação).

É usado um modelo fork & join, em que temos inicialmente uma única thread (a master, ou principal), e depois temos pontos da aplicação em que um grupo de threads é criado (fork, no início de uma região paralela), e pontos da aplicação onde as threads são terminadas voltando a haver apenas uma thread (join, no final de uma região paralela), como mostrado na Figura 1. Ou seja, as regiões paralelas identificam os blocos de código onde o trabalho a ser executado é atribuído a várias threads. A norma do OpenMP prevê a possibilidade de termos regiões paralelas aninhadas, em que cada thread de um grupo de threads dá origem a um novo grupo de threads. No entanto, nem todas as implementações suportam regiões paralelas (e neste caso, cada região paralela aninhada será apenas executada por uma thread).

OpenMP: modelo fork & join
Figura 1: modelo fork & join do OpenMP

O acesso às funcionalidades do OpenMP é feito através de três tipos de mecanismos:

directivas de compilador
permitem criar regiões paralelas, distribuir o trabalho a ser realizado pelas várias threads, controlar o acesso a dados (especificando se são privados ou partilhados, por exemplo), assim como gerir a sincronização entre threads;
funções
permitem definir o número de threads a usar, obter informações sobre quantas threads estão a correr ou qual o identificador da thread em que estamos, determinar se se está numa zona paralela, gerir locks, aceder a funções de tempo (relógio), entre outras coisas;
variáveis de ambiente
permitem também definir o número de threads a usar, controlar o escalonamento, definir o máximo aninhamento de regiões paralelas, etc.

Nas próximas secções veremos como alguns alguns destes mecanismos podem ser usados, sendo dado especial foco às directivas de compilador.