Você está na página 1de 31

Capı́tulo

1
Computação de Alto Desempenho

Murilo Boratto 1 , Domingo Giménez2 e Leandro Coelho3

Abstract

The following text consists of the class material of the Laboratory of High Perfor-
mance Computing, shown in XI ERBASE 2011. The text is divided into topics: The
main innovation of parallel computing; The art of parallel programming via exam-
ples; and Current trends in research and technology in Parallel, Distributed and Grid
computing.

Resumo

O seguinte texto consiste no material didático das sessões das aulas de Laboratório de
Computação de Alto Desempenho, apresentadas no XI ERBASE 2011. Este docu-
mento esta dividido em tópicos que abordam: Os principais avanços em computação
paralela; A arte de programar em paralelo através de exemplos; e As tendências
atuais em pesquisa e tecnologia de Computação Paralela, Distribuı́das e de Grids.

1.1. Introdução
Podemos afirmar que o processamento paralelo era utilizado desde o surgi-
mento dos primeiros computadores na década de 50 [1]. Em 1955, o IBM 704 inclui
um hardware para processamento de números de ponto flutuante (co-processador).
Em 1956, a IBM lança o projeto 7030 (conhecido como STRETCH) para produzir
um “supercomputador” para o Los Alamos National Laboratory (LANL). O objeti-
vo, na época era construir uma máquina com capacidade computacional 100 vezes
1 Colegiado de Engenharia da Computação (CECOMP), Universidade Federal do Vale do Sao
Franscisco, Juazeiro, Bahia, Brasil, murilo.boratto@univasf.edu.br
2 Departamento de Informática y Sistemas (DIS), Universidad de Murcia, Murcia, Espanha,

domingo@um.es
3 Núcleo de Arquitetura de Computadores e Sistemas Operacionais (ACSO), Universidade do

Estado da Bahia, Salvador, Bahia, Brasil, leandrocoelho@uneb.br


maior do que qualquer outra máquina disponı́vel. No mesmo ano, o projeto LARC
(Livermore Automatic Research Computer ) começa a projetar um outro “super-
computador” para o Lawrence Livermore National Laboratory (LLNL). Estes dois
projetos levaram três anos para produzir os seus produtos: os supercomputadores
conhecidos como STRETCH e LARC.
Concomitante a estas iniciativas, muitos outros projetos produziram máqui-
nas paralelas com as mais variadas arquiteturas e diferentes tipos de software. As
principais razões para a construção de máquinas paralelas são: diminuir o tempo to-
tal de execução de uma aplicação, conseguir resolver problemas mais complexos, de
grandes dimensões, e prover concorrência, ou seja, permitir a execução de diferentes
tarefas de forma simultan̂ea.
Existem ainda diversas outras razões para a computação paralela: tirar van-
tagem de recursos não locais (e.g., utilização de recursos que estejam numa rede de
longa distaância - WAN ou na própria Internet, quando os recursos locais são es-
cassos), diminuir custos (e.g., ao invés de pagar para utilizar um supercomputador,
poderı́amos utilizar recursos baratos disponı́veis remotamente), ultrapassar limites
de armazenamento: memória e disco (i.e., para problemas de grandes dimensões,
usar memórias de múltiplos computadores pode resolver o problema da memória
limitada presente em uma única máquina).
Finalmente, podemos citar uma última razão: os limites fı́sicos de desempenho
de uma máquina seqüencial, que atualmente já está na fronteira do que pode ser
praticável em termos de velocidades internas de transmissão de dados e de velocidade
de CPU. Além disso, podemos também dizer que durante os últimos 10 anos, as
tendências têm sempre apontado para um futuro em que a computação paralela está
sempre presente, dado que as redes de interconexão têm avançado significativamente
em termos de velocidade de comunicação e largura de banda.
Nestas sessões de Laboratório de Computação de Alto Desempenho, fala-
remos dos principais avanços em Computação Paralela, da arte de programar em
paralelo através de exemplos, e das tendências atuais em pesquisa e tecnologia em
Computação Paralela e Distribuı́da.

1.2. Estrutura Detalhada do Curso


1.2.1. Objetivos do Curso
As aulas de Laboratório em Computação de Alto Desempenho, tem como
principais objetivos:

 Demonstrar a importância e a inovação da Computação de Alto Desempenho.


 Propiciar o entendimento dos conceitos de Computação Paralela e Distribuı́da.
 Apresentar e proporcionar a prática de técnicas e estratégias de estı́mulo ao
desenvolvimento de softwares paralelos.
 Enfatizar a exploração de estratégias para as diversas plataformas de execução
em paralelo.
 Aplicar o conhecimento em situações praticas, notadamente na formulação de
estratégias para problemas reais.

1.2.2. Tipo de Curso


O Laboratório tem um foco prático, com o assunto dividido em um determi-
nado número de sessões de aula, acompanhadas de um texto base, onde se resumem
as noções tratadas no curso e se incluem uma serie de trabalhos práticos a serem
realizados pelos alunos.
A proposta educacional desse curso está elaborada de forma a propiciar uma
rápida e fácil absorção dos conteúdos propostos, mas sem deixar de lado a quantidade
e a qualidade dos temas.
O curso esta dividido em 3 partes distribuı́das em 5 sessões:

 Parte 1 - Introdução a Computação Paralela: Perspectivas e Aplicações (Sessão 1)


 Parte 2 - Programação OpenMP (Sessões 2 e 3)
 Parte 3 - Programação MPI (Sessões 4 e 5)

1.2.3. Material de Curso


Além desse resumo, o material utilizado pelo curso consistirá nos slides dispo-
nı́veis (en http://dis.um.es/~domingo/investigacion.html) e nos códigos fontes
de exemplificação que acompanham o livro [2], o qual se encontra como material de
apoio na pagina da editorial (http://www.paraninfo.es/).

1.2.4. Detalhes dos Temas Expostos nas Partes


 Computação Paralela: Perspectivas e Aplicações: Nessa sessão inicial
falaremos dos principais avanços em Computação Paralela, e da arte de progra-
mar em paralelo, e das tendências atuais em pesquisa e tecnologia em Compu-
tação Paralela e Distribuı́da e dos conceitos de Computação em Grid.
 Programação OpenMP: OpenMP [3] é uma API (Application Program
Interface) que possibilita a programação paralela em ambientes multiprocessa-
dos com memória compartilhada, como é o caso da maiorias dos processadores
lançados no mercado atualmente. Utilizando modificações em compiladores,
esta tecnologia permite o desenvolvimento incremental de aplicações paralelas
a partir de código fonte sequencial. Esta norma técnica é definida por um con-
sórcio que reúne importantes fabricantes de hardware e software. O objetivo
desse módulo será a apresentação das noções básicas de programação OpenMP.
A metodologia consistirá em apresentar uma introdução dos conceitos teóricos
da programação em OpenMP seguidos por uma descrição de como preparar
um ambiente computacional para o desenvolvimento de aplicações.
 Programação MPI: MPI [4] (acrónimo de Message Passing Interface) é uma
proposta de padronização para a interface de troca de mensagens para ambien-
tes paralelos, especialmente aqueles com memória distribuı́da. Neste modelo,
uma execução compreende um ou mais processos que se comunicam chamando
rotinas de uma biblioteca para receber e enviar mensagens para outros pro-
cessos. O objetivo desse módulo será a apresentação das noções básicas de
programação MPI. A metodologia consistirá em apresentar uma introdução
aos conceitos teóricos da programação em MPI seguidos por uma descrição de
como preparar um ambiente computacional para o desenvolvimento de apli-
cações.

1.3. Computação Paralela: Perspectivas e Aplicações


Nesta seção serão introduzidas as primeiras idéias gerais da Computação
Paralela, assim como os diferentes enfoques deste tipo de programação e algumas
áreas de trabalho e aplicação.

1.3.1. Tipos básicos de Computação Paralela


A Computação Paralela consiste na exploração de vários processadores per-
mitindo que estes trabalhem de forma conjunta na resolução de um problema compu-
tacional. Normalmente, cada processador trabalha em uma parte de um determinado
problema havendo, muitas vezes, a necessidade de troca de informação (e.g., dados)
entre os processadores. Dependendo da maneira que for realizada essa troca pode-se
produzir dois modelos de programação paralela: Memória Compartilhada e Memória
Distribuı́da:

 Memória Compartilhada
O modelo de Memória Compartilhada (Shared Memory Model) será iden-
tificado quando existir uma porção de memória que possa ser acessada dire-
tamente por todos os elementos de um conjunto de processos. Esta memória
será utilizada para transferência de informação entre os mesmos. Este tipo de
modelo corresponde a sistemas que possuem um conjunto de memória com-
partilhada com todos os processadores envolvidos, onde a memória estaria
distribuı́da no sistema, entre os distintos processadores. Existem ferramentas
especı́ficas de programação em memória compartilhada. As mais conhecidas
são pthreads [5], Threading Building Blocks [6], OpenMP [7, 3, 8]. Esta última
pode ser considerada, na atualidade, o padrão para este tipo de programação
e será alvo de estudo deste curso.

 Memória Distribuı́da
No modelo de Memória Distribuı́da (Distributed Memory Model) cada pro-
cessador tem associado um bloco de memória próprio. Assim, cada elemento
pode acessar indiretamente um dos blocos de memórias associados a outros
processadores. Desta forma, para conseguir a troca de dados é necessário que
cada processador realize explicitamente a solicitação de dados aos processado-
res disponı́veis, que serão os responsáveis pelo envio dos dados (i.e., resposta).
Este modelo se baseia na técnica de Passagem de Mensagem. Existem vá-
rios ambientes de programação para esse modelo (e.g., PVM [9], BSP [10]) e
o estándar atual chama-se MPI [4, 11], o qual será abordado neste seminário.
1.3.2. Necessidade da Computação Paralela
A necessidade da Computação Paralela é provocada pelas limitações de ex-
pansão dos computadores seqüenciais (i.e.,limites fı́sicos de hardware) e possibilida-
de de expansão da capaciadade de processemanto mediante agrupação de diferentes
computadores: integrando múltiplos processadores pode-se executar vários processos
simultaneamente para solucionar problemas que exigem mais memória ou um maior
poder de computação.
Outro fator que justifica a necessidade da computação paralela é a relação
custo/benefı́cio proporcionada. Há razões econômicas para que o preço dos compu-
tadores seqüenciais não seja proporcional à sua capacidade de computação. Para
adquirir uma máquina com o dobro de capacidade computacional, normalmente é
necessário o investimento de mais que o dobro do valor da mesma. Já na computação
paralela, a conexão de múltiplos processadores através de uma rede de interconexão
permite a obtenção do aumento no desempenho de forma proporcional ao número
de processadores envolvidos, com um custo mı́nimo adicional.
Estas caracterı́sticas, aliadas as novas e crescentes demandas das aplicações
emergentes (e.g., Vı́deo sob Demanda, Processamentos Bio-fı́sicos, Simulações de
tempo real,...), que demandam quantidades de recursos computacionais elevadas
dificultam a sobrevida de sistemas sequenciais.
A programação paralela é uma solução para resolver estes problemas, mas
apresenta outras dificuldades. Alguns desafios são fı́sicos, tais como: dificuldade em
integração de componentes; dissipação de calor associados; e aumento da comple-
xidade no acesso aos dados. Estas questões podem se tornar pontos de estrangu-
lamento, o que irá tornar difı́cil a obtenção de bons desempenhos, porém, se bem
administrados, podem aumentar a capacidade computacional do sistema.
Há também problemas lógicos, tais como: maior dificuldade no desenvolvi-
mento de compiladores; e existência de um ambiente eficiente de programação para
sistemas paralelos. Estes problemas são mais complexos de serem resolvidos em sis-
temas paralelos que em sistemas seqüencias: programar em paralelo é muito mais
complexo e difı́cil do que programar seqüencialmente. Não obstante, um programa
em paralelo é utilizado para reduzir a resolução temporal de problemas computacio-
nais, para resolver problemas de grande escala que não podem ser resolvidos por um
processo seqüencial. Para isto, faz-se necessário a utilização da computação de alto
desempenho através de algoritmos paralelos que utilizem os sistema de forma eficaz
e eficiente.
Os problemas que são tratados em paralelo são, em geral, de um tipo especı́fi-
co: problemas de alto custo computacional (i.e., problemas que demandam utilização
de grandes quantidades de recursos computacionais como memória, processamento
e armazenamento); e, problemas que envolvem determindado prazo máximo de exe-
cução (i.e., problemas de tempo real). Assim, a comunidade cientı́fica utiliza compu-
tação paralela para resolver estes problemas que, sem a computação paralela, seriam
inviáveis de serem solucionados.
Como exemplo de algumas áreas do conhecimento que podem ser beneficiadas
com a utilização da programação paralela, dentre outros, podemos citar: estudos
meteorológicos, através das previsões e estudos da climatologia; o estudo do genoma
humano, a modelagem da biosfera; as predições sı́smicas; e a simulação de moléculas.

1.3.3. Paralelismo em computadores seqüenciais


A idéia de implementar o paralelismo não é exclusiva dos multicomputadores
ou dos clusters de computadores4 , já sendo utilizado em diferentes formas de sistemas
computacionais seqüenciais desde o desenvolvimento dos primeiros computadores:

 A Segmentação consiste na decomposição das instruções em uma série de


partes mais simples, que se executam na forma de pipeline 5 da maneira que ao
mesmo tempo se pode estar trabalhando em várias instruções diferentes, em
partes diferentes da segmentação.

 É possı́vel dispor de múltiplas unidades funcionais, que levam a cabo as distin-


tas operações ao mesmo tempo, e algumas delas especializadas em operações
de um certo tipo, como podem ser os processadores matemáticos.

 O paralelismo a nı́vel de instrução consiste en possibilitar a execução


de várias instruções ao mesmo tempo. Podem ser utilizadas diversas técnicas,
como a segmentação ou o uso de várias unidades funcionais, e pode-se combinar
as diferentes técnicas entre sı́.

 A memória se divide en blocos, de maneira que é possı́vel estar acessando ao


mesmo momento, blocos diferentes, possivelmente em um bloco lendo e em
outro, escrevendo.

 A memória está organizada hierarquicamente, com diferentes velocidades de


acesso, segundo o nı́vel em que se encontram. Tipicamente, o acesso é mais
rápido nos registros, no próximo nı́vel estão as memórias cache, a continuação
a memória principal, e por último a memória secundária. Assim, uma vez que
um bloco de memória é acessado, este passa para memória cache, mais próxima
ao processador, e o trabalho com esses dados ocorrerão mais rápido, enquanto
que se pode estar acessando a zonas de memória em outro nı́vel das hierarquias
para atualizar os dados recém modificados.

 A execução fora de ordem consiste em detectar no código instruções que


não dependem umas das outras, e executá-las em uma ordem diferente da que
aparecem.

 Os processadores vetoriais dispõem de unidades vetoriais, que podem tra-


tar simultaneamente vários dados de um vetor. São máquinas que possuem
4 Conjunto de computadores interligados por uma rede de interconexão, também conhecidos com
CoHNoW, Collection of Heterogeneous of Workstations
5 Método de processamento que visa a divisão funcional da unidade central de processamento

para permitir que a utilização da mesma por mais de uma instrução (paralelismo de instrução) em
paralelo, imitando a linha de montagem das industrias
um conjunto de processadores que operam de forma paralela e sı́ncrona, exe-
cutando normalmente a mesma função. Os processadores de uma máquina
vetorial são chamados de Unidades de Processamento (EP) e trabalham sob a
supervisão de uma única Unidade de Controle (UC).
 É também usual encontrar co-processadores de entrada/saı́da. Estes compo-
nentes hardware permitem operações de E/S simultánemente com as operações
de processamento (i.e., computação).

Esta lista, ainda que, não muito extensa, dá idéia da importância da noção
de paralelismo e da sua utilização no desenho de arquiteturas seqüenciais para ace-
lerar o processamento. O estudo detalhado da arquitetura dos computadores e da
utilização do paralelismo tanto em sistemas seqüenciais tanto em paralelos não se-
rá abordada neste curso, mas há inúmeros livros que abordam este tema de uma
maneira exaustiva e podem ser consultados em [12, 13, 14].
Por outro lado, a lei de Moore ([15, 16]) diz que o a capacidade de processa-
mento dos processadores integrados dobra a cada 18 meses. Isto produz um incre-
mento na velocidade de execução de programas, mas se observarmos,comprovamos
que este aumento é conseguido, na atualidade, apenas pelos processadores Dual Core
da Intel, que incluem dois núcleos e que necessitam portanto da programação para-
lela de forma explı́cita para poder obter os máximos desempenhos que estes sistemas
podem oferecer. Esse tipo de processadores são usados como componentes básicos
nos computadores que são comercializados na atualidade, o que nos permite afir-
mar que a programação básica nos processadores atuais necessita da programação
paralela como base para explorar toda a potencialidade oferecida pelo hardware.

1.3.4. Modelos clássicos de computação


A classificação dos sistemas paralelos mais conhecida é a taxonomı́a de Flynn
[17], que os classifica segundo o fluxo dos dados e das instruções:

 O modelo SISD (Single Instruction Single Data) corresponde ao caso da má-


quina seqüencial (i.e., Modelo base a computação moderna, proposto por John
Von Neumann). Possui um único fluxo de instruções que é tratado consecu-
tivamente, e é trabalhado sobre um único conjunto de dados. Sabemos que
os processadores seqüenciais não seguem exatamente este modelo, já que os
dados se agrupam em blocos diferentes aos que se pode acessar simultanea-
mente, sendo introduzido paralelismo na execução das instruções, por exemplo,
com segmentação, com o uso de múltiplas unidades funcionais ou de unidades
vetoriais.
 O modelo SIMD (Single Instruction Multiple Data) é considerado um único
fluxo de instruções mas atua simultáneamente sobre vários conjuntos de dados.
É um modelo paralelo que trabalha com vários elementos do processo, execu-
tando em cada momento a mesma instrução, porém trabalhando sobre dados
diferentes. Por exemplo, cada processo poderia estar realizando operações de
soma dos dados de um vetor, mas cada um atuaria sobre un vetor diferente.
 No modelo MISD (Multiple Instruction Single Data) são executados vários
fluxos de instruções ao mesmo tempo atuando todos sobre o mesmo conjunto
de dados. Não existem referências sobre este modelo.

 A grande maioria dos sistemas paralelos, e em particular dos sistemas de pro-


pósito geral, seguem o modelo MIMD (Multiple Instruction Multiple Data),
onde se tem várias unidades de processo, cada uma com um conjunto de da-
dos associado e executando um fluxo de instruções diferentes. Se temos vários
núcleos que compartilhem a memória e vários threads que se atribuem aos
núcleos, os threads trabalham de maneira independente ainda que executem
o mesmo código, já que em qualquer momento threads diferentes vão utilizar
instruções diferentes do código, e além disso podem acessar zonas de dados
que compartilhem em memória.

O modelo que utilizamos neste curso é o MIMD, que é o que segue os mul-
ticomputadores atuais, não importando o paradigma de programação: por memó-
ria compartilhada ou por envio de mensagens. Também, consideraremos o modelo
SPMD (Single Program Multiple Data), modelo em que todos os threads ou pro-
cessos executam o mesmo programa más sem sincronizar a execução das instruções:
cada elemento do processo executa as instruções no seu próprio ritmo, mesmo que
em alguns pontos pode haver sincronização dos processos. Nas sessões seguintes
analisaremos a programação paralela com OpenMP e MPI para sistemas homogê-
neos, apesar que esse tipo de programação também usa como base outros sistemas
já anteriormente mencionados, e analisaremos alguns exemplos de combinações de
OpenMP e MPI para a Programação Hı́brida.
1.4. Programação OpenMP
OpenMP é um padrão atual para a programação utilizando memoria compar-
tilhada, que incluem os sistemas multicores e computadores de altas prestações com
memoria virtual compartilhada. Nesta sessão analisaremos as caracterı́sticas básicas
do OpenMP utilizando os exemplos que se encontram em http://www.paraninfo.es/.

1.4.1. Exemplo básico: Aproximação da integral definida


Um exemplo tı́pico é o cálculo do valor de π por integração numérica. O valor
de π pode se aproximar com a integral:

Z 1
1 π
2
dx =
0 1+x 4
Uma das soluções adotadas seria aproximar a área da integral a áreas de
retângulos de uma certa base. Quanto menor for a base, mais retângulos teremos,
logo haverá uma melhor aproximação ao valor final da área. O código código3-
1.c é um programa seqüencial para este problema, que contém um loop for que se
acumulam as áreas dos retângulo com os que se aproximam da integral.
Uma versão paralela do mesmo problema para OpenMP pode-se encontrar no
código código3-16.c. Se consegue incluir ao código o paralelismo, apenas indicando
a forma com que se deve distribuir o trabalho dos loop as diferentes threads. Aqui
aparecem algumas interfaces C de OpenMP:

 Se deve incluir a biblioteca OpenMP (omp.h).

 O diretiva de paralelismo para OpenMP junto a interface C é indicada com


#pragma omp.

 A diretiva parallel indica que se inicializam vários threads para trabalharem


em paralelo dentro do bloco de sentenças no loop for.

 Tem algumas diretivas de compartilhamento de variáveis dentro do bloco de


sentenças.

O modelo de execução de OpenMP é o modelo fork-join. A execução do


código3-16.c teria os seguintes passos:

 Inicialmente, quando se executa o código, ele trabalha com um único thread,


que tem uma série de variáveis (int n, i; double pi, h, sum, x;) que
estão na memória do sistema.

 Este thread pede o número de intervalos a serem usados e inicializa as variáveis


h y sum. Este trabalho se faz em seqüencial ao trabalhar um único thread.

 Ao chegar ao construtor #pragma omp parallel inicializam-se vários threads


escravos (parte fork do modelo). O thread que trabalha inicialmente é o thread
mestre do conjunto de escravos. Os threads estão numerados desde 0 ate o
número de threads-1. O mestre e os escravos trabalham em paralelo no bloco
que aparece a continuação do construtor.
 Ao ser um construtor parallel for o que se paraleliza é o trabalho que se
divide entre o conjunto de threads para o loop for. Como o loop tem n passos,
se dispomos por exemplo de 4 threads, a divisão do trabalho consiste em
atribuir a cada thread n/4 passos do loop. Como não se indica como realizou-
se a divisão, se atribui os n/4 primeiros passos a thread 0, os seguintes n/4
passos a thread 1, e assim sucessivamente.
 Todas as variáveis da memória (n, i, pi, h, sum, x) consideram-se em
uma memória global as quais podem acessar todos os threads, mas algumas
variáveis se indica que são privadas aos threads (private(x, i)), outra se diz
que são compartilhadas de uma maneira especial (reduction(+:pi)), e das
que não se diz nada (n, h, sum) são compartilhadas.
 Cada thread tem um valor diferente de i pois cada um realiza cálculo para
valores diferentes de x porque calculam áreas de retângulos diferentes.
 Ao acabar o loop for há sincronização de todos os threads, e os escravos
morrem ficando somente o thread mestre (parte join do modelo).
 O mestre, trabalhando em seqüencial, é o que calcula o valor final e os mostra
por tela.

1.4.2. Compilação e execução


Vamos ver com este primeiro exemplo como se compila e se executa em
paralelo um código OpenMP.
É necessário dispor de um compilador que possa interpretar os pragmas que
aparecem no código. O gcc tem esta capacidade desde a versão 4.1. Também po-
demos dispor de versões comerciais, como o compilador icc da Intel. A opção de
compilação em gcc é -fopenmp ou -openmp e em icc é -openmp. Assim, se compi-
larmos com:
gcc -O3 -o codigo3-16 codigo3-16.c -fopenmp
cria-se codigo3-16, que poderá ser executado em paralelo inicializando várias th-
reads. A execução se realiza como em qualquer outro programa, mas tem-se que
determinar quantas threads interferirão na região paralela. Existe uma variável de
entorno (OMP_NUM_THREADS) que nos indica esse número. Se não se inicializa essa
variável teremos um valor por defeito, que costuma-se coincidir com o número de
núcleos do nodo onde estivermos trabalhando. Uma outra possibilidade é fazer um
export OMP_NUM_THREADS=6, com que estabelecemos o número de threads na região
paralela a seis, independentemente do número de núcleos de que tenhamos.
Podemos experimentar com umaúnica thread e executar o programa tomando
tempos com dados de entrada de tamanho variável, e a continuação variar o número
de threads e tentar medir tempos de execução com a mesma entrada:
export OMP_NUM_THREADS=1
time código3-16 <in10000
export OMP_NUM_THREADS=2
time código3-16 <in10000
Observamos que os tempos de execução em sequencial não levem mais tempo
em ser executado do que em paralelo, isto pode dever-se a se somente tivermos apenas
um núcleo no processador, logo não podemos resolver o problema em paralelo mais
rápido que em sequencial. Ainda que dispuséssemos de vários núcleos o tamanho do
problema (o número de intervalos) pode não ser suficientemente grande como para
que se note o efeito da paralelização. Se executamos o experimento anterior com
tamanhos grandes (por exemplo com in10000000) podemos chegar a execuções em
que o uso do paralelismo reduza o tempo de execução.

1.4.3. Formato das diretivas


As diretivas OpenMP seguem as convenções dos standards para diretivas de
compilação em C/C++, são case sensitive, somente podem especificar-se um nome
de diretiva, e cada diretiva se aplica, ao menos, a sentença que segue, que pode
ser um bloco estruturado. Em diretivas largas podem continuar-se na seguinte linha
fazendo o uso de caracteres \ ao final da linha.
O formato geral é o seguinte:
#pragma omp nombredirec. [clausulas, ...] nova-linha
onde:

 #pragma omp. Requer-se em todas as diretivas OpenMP para C/C++.


 nombredirec.. É um nome válido de diretiva, e deve aparecer depois do prag-
ma e antes de qualquer cláusula. Em nosso exemplo parallel for.
 [cláusulas, ...]. Opcionais. As cláusulas podem ir em qualquer ordem e
repetir-se quando seja necessário, ao menos que haja alguma restrição. Em
nosso caso são reduction e private.
 nova-linha. É obrigatório separar a linha que contem ao pragma do bloco
estruturado ao que afeta.

1.4.4. Criação de threads


Como indicou antes, a diretiva com que se criam threads escravos é parallel:
#pragma omp parallel [clausulas]
bloco
onde:

 Se cria um grupo de threads e o thread inicializado atua de mestre.


 Com a cláusula if se avalia a expressão e dar-se um valor diferente de zero
criando os threads, e se o valor é zero, se executa em seqüencial.

 O número de threads quando criados se obtém através da variável de entorno


OMP_NUM_THREADS ou com chamadas a biblioteca (veremos a continuação como
se faz).

 As cláusulas de compartilhamento das variáveis que suporta a diretiva para-


llel são: private, firstprivate, default, shared, copyin e reduction.

Os programas código3-11.c e código3-12.c mostram o uso da diretiva


parallel com o tı́pico exemplo “Hello world”. Além do mais se mostram algumas
das funções da biblioteca OpenMP.
Em código3-11.c:

 Cada um dos threads que trabalha na região paralela possui seu identifi-
cador de thread (que esta entre 0 y OMP_NUM_THREADS-1) usando a função
omp_get_thread_num, e o guarda em sua copia local de tid.

 Por outro lado, todos obtém o número de threads que existe na região cha-
mando a omp_get_num_threads, que devolve o número de threads que se esta
executando dentro de uma região paralela. Caso se chama-se estaúltima função
desde uma região seqüencial o resultado seria 1, pois somente se está execu-
tando o thread mestre. Como todos escrevem o mesmo valor, a ordem em que
os threads se atualiza nthreads não importa; mas o acesso a variável compar-
tilhada supondo um tempo de execução adicional pela gestão do sistema de
acesso as variáveis compartilhadas.

 Finalmente cada thread escreve na tela. Como a execução é paralela as mensa-


gens se podem intercalar na saı́da. A diferença do
código3-16.c, é que aqui comprovamos o número de threads que se esta
executando.

No exemplo código3-12.c vemos que:

 Ao chamar omp_get_num_threads desde fora de uma região paralela o resul-


tado é 1.

 Executam-se duas regiões paralelas, e todo o código que esta fora delas se
executa em seqüencial pela thread mestre.

 A função omp_set_num_threads determina o número de threads que trabal-


hara nas seguintes regiões paralelas. Na primeira região se executam 4 e na
segunda 3. O valor estabelecido por esta função tem-se prioridade sobre o valor
de variável OMP_NUM_THREADS.
1.4.5. Funções e variáveis
Nos exemplos anteriores vimos o uso das funções
omp_set_num_threads, omp_get_num_threads e omp_get_thread_num.
Outras funções são:
omp_get_max_threads: obtém a máxima quantidade possı́vel de threads.
omp_get_num_procs: devolve o máximo número de processadores que se podem
atribuir ao programa.
omp_in_parallel: devolve um valor diferente de zero caso se execute dentro de uma
região paralela.
Existem funções para a sincronização de threads:
void omp_init_lock(omp_lock_t *lock)
void omp_init_destroy(omp_lock_t *lock)
void omp_set_lock(omp_lock_t *lock)
void omp_unset_lock(omp_lock_t *lock)
int omp_test_lock(omp_lock_t *lock)

Existe 4 variáveis de entorno. Além da OMP_NUM_THREADS:


OMP_SCHEDULE
OMP_DYNAMIC
OMP_NESTED

1.4.5.1. Funções da biblioteca OpenMP

O standard OpenMP define uma API para chamadas a funções de bibliotecas


que permitem lograr uma grande variedade de funções. Assim, encontramos funções
para averiguar o número de threads e processos, para estabelecer o número de th-
reads a serem utilizados, funções de âmbito geral que permitem a criação e gestão de
semáforos, funções para temporização e medição de tempos, funções para paralelis-
mo e para gestão dinâmica de threads. Mencionaremos nesta sessão de formaúnica
algumas das funciones básicas que se utilizam de forma generalizada nos programas
OpenMP. Para C/C++ é necessário incluir o arquivo omp.h.

 void omp_set_num_threads(int num_threads): estabelece o número de th-


reads a serem utilizados nas regiões paralelas.

 int omp_get_max_threads(void): devolve o número máximo que pode ser


devolvido pela função int omp_get_num_threads(void).

 int omp_get_thread_num(void): devolve o número de thread feito pelo pro-


grama. Este número toma valores entre 0 e omp_get_num_threads() - 1.

 int omp_get_num_procs(void): devolve o número de processadores fı́sicos


disponı́veis pelo programa.
1.4.6. Definindo regiões paralelas
Uma região paralela é um bloco de código que se executará por várias threads
simultaneamente. Cada thread executa um bloco de código de forma separada. O
bloco completo será executado por cada thread de execução de forma redundante, a
menos que se especifique o contrario. O construtor tem a seguinte forma:

#pragma omp parallel [clausulas...]


bloco-estruturado

Onde as cláusulas podem ser:

 if (expressao-escalar)

 private (lista-de-variaveis)

 shared (lista-de-variaveis)

 default (shared | none)

 firstprivate (lista-de-variaveis)

 reduction (operador: lista-de-variaveis)

 copyin (lista-de-variaveis)

O código a seguir ilustra o conceito de região paralela.

#include <omp.h>

int main (int argc, char **argv) {

int nthreads, tid;


printf("Trabalharemos con 4 threads");
omp_set_num_threads(4);
nthreads = omp_get_num_threads();
printf("Numero de threads em execuçao = %d\n", nthreads);

#pragma omp parallel private(tid)


{
tid = omp_get_thread_num();
printf("Ola desde a thread = %d\n", tid);

if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Numero de threads = %d\n", nthreads);
}
}

printf("Trabalharemos agora com 3 threads");

omp_set_num_threads(3);
nthreads = omp_get_num_threads();
printf("Numero de threads en execuçao = %d", nthreads);

#pragma omp parallel


{
tid = omp_get_thread_num();
printf("Ola desde o thread = %d", tid);

if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Numero de threads = %d", nthreads);
}
}
}

O programa produz a seguinte saı́da:

Trabalhamos com 4 threads


Numero de threads em execuçao = 1
Ola desde a thread = 0
Numero de threads = 4
Ola desde a thread = 1
Ola desde a thread = 2
Ola desde a thread = 3
Trabalhamos agora com 3 threads
Numero de threads en execuçao = 1
Ola desde a thread = 0
Ola desde a thread = 1
Numero de threads = 3
Ola desde a thread = 2

1.4.7. Executando sessões de código em paralelo


A diretiva sections especifica quais códigos dos blocos agrupados pela dire-
tiva sera divida entre os threads. Tem-se que primeiro finalizar uma região paralela.
A forma do construtor é:
#pragma omp sections [clausulas ...]
{

#pragma omp section


bloco-estruturado

#pragma omp section


bloco-estruturado

Onde o conjunto de cláusulas pode ser:

 private (lista-de-variaveis)

 firstprivate (lista-de-variaveis)

 lastprivate (lista-de-variaveis)

 reduction (operador: lista-de-variaveis)

 nowait

Com o código a seguir ilustramos o uso da diretiva sections. Nele dispomos


de 4 sessões paralelas que se atribuirão aos threads de execução.

#include <omp.h>

int main (int argc, char **argv) {


int nthreads, tid;
// A variavel tid eh privada a cada thread
#pragma omp parallel private(tid)
{
#pragma omp sections
{
#pragma omp section
{
tid = omp_get_thread_num();
nthreads = omp_get_num_threads();
printf("O thread %d, de %d, calcula a section 1",
tid, nthreads);
}
#pragma omp section
{
tid = omp_get_thread_num();
nthreads = omp_get_num_threads();
printf("O thread %d, de %d, calcula a section 2",
tid, nthreads);
}
#pragma omp section
{
tid = omp_get_thread_num();
nthreads = omp_get_num_threads();
printf("O thread %d, de %d, calcula a section 3",
tid, nthreads);
}
#pragma omp section
{
tid = omp_get_thread_num();
nthreads = omp_get_num_threads();
printf("O thread %d, de %d, calcula a section 4",
tid, nthreads);
}

} //sections
} //parallel section
}

A execução do código anterior produz a seguinte saı́da, sendo que cada thread foi
atribuı́do a uma das sessões paralelas, com um número total de threads igual a 4.

O thread 0, de 4, calcula a section 1

O thread 1, de 4, calcula a section 2

O thread 2, de 4, calcula a section 3

O thread 3, de 4, calcula a section 4


1.5. Programação por Passagem de Mensagem
Neste modelo de programação um determinado processo que necessita infor-
mação de outro é obrigado a solicitar a mesma através do envio de uma mensagem
até o processo detentor da informação. Esta comunicação é viabilizada por um midd-
leware que realiza a interface entre os processos.
Podemos aplicar o modelo de passagem de mensagem a uma arquitetura
fracamente acoplada (ie., um conjunto de computadores interconectados via rede) ou
mesmo a arquiteturas fortemente acopladas como as arquiteturas multicore atuais.
Não obstante, a utilização deste modelo é amplamente difundida no primeiro caso e é,
atualmente a base para a computação de alto desempenho através da implementação
de clusters 6 , multi-clusters e grids computacionais.
Clusters de estações de trabalho são uma alternativa barata e disponı́vel,
para computadores de alto desempenho especializados e torna-se uma das principais
alicerces para a computadores de alto desempenho com baixo custo. A largura de
banda para comunicação entre workstations cresceu muito nas novas tecnologias de
rede além dos protocolos, que podem ser implementados em redes locais ou mesmo
em redes de longa distância, o que gera certa facilidade de integração dentro das
redes existentes atualmente.
Devido as caracterı́sticas supra-citadas de um cluster computacional, a de-
manda por middlewares confiavéis e robustos torna-se mais constante. Diferentes
abordagens foram desenvolvidas para atuar neste modelo [9], [10] [4, 11]. Dentre as
diferentes soluções encontradas na literatura, o MPI [4, 11] tornou-se um padrão de
fato e vem sendo utilizado pelos grandes centros de pesquisa de todo o mundo.

1.5.1. MPI: Message Passing Interface


Segundo [18], A Interface de Passagem de Mensagem (MPI) provê uma ba-
se poderosa para construir programas paralelos. Uma de suas metas de projeto é
possibilitar a construção de bibliotecas de software paralelas, ajudando a resolver o
problema de desenvolvimento de aplicações paralelas.
Dentro do padrão MPI, uma aplicação será composta por diferentes processos
que irão trocar informaçõesúteis através de envios e recebimentos de menssagens.
O MPI é uma biblioteca baseada em passagem de mensagens. Pode-se utilizar
como interfaces as linguagens de programação C ou Fortran, desde que a partir de um
código escrito em uma dessas linguagens se pode gestionar processos e comunica-los
entre si. MPI não é aúnica biblioteca que disponibiliza a comunicação por passagem
de mensagem, mas considera-se o padrão atual para esse tipo de programação.
A biblioteca PVM (Parallel Virtual Machine) é anterior a MPI, também tem
a mesma funcionalidade, onde inicialmente foi desenvolvida para redes de compu-
tadores, no qual se incluı́am módulos de tolerância a falhas, facilidades de criação de
processos, ..., enquanto que MPI surgiu como uma especificação para as máquinas

6 é
uma coleção de máquinas que se utilizam das redes de computadores comerciais, locais e/ou
remotas para paralelizar suas transações
paralelas utilizando passagem de mensagem.
Inicialmente o MPI não continha algumas facilidades implementadas no PVM,
como: os módulos de tolerância a falhas e a orientação a clusters, mas com o tempo
foram feitas algumas evoluções e hoje já existe MPI orientado a tolerância a falhas
(FT-MPI [19]) e a sistemas heterogéneos (HeteroMPI [20], MPICH-Madeleine [21]).
Nesta sessão analisaremos as caracterı́sticas gerais da especificação padrão
do MPI, publicada em 1994. Esta especificação foi desenvolvida pelo MPI Forum,
formado por um conjunto de universidades e empresas que especificaram as funções
que deveriam estar contidas na biblioteca de passagem por mensagem. A partir dessa
especificação os fabricantes de multicomputadores incluı́ram implementações espe-
cificas para seus equipamentos, aparecendo varias implementações livres, as versões
mais difundidas são: MPICH [22] e LAM-MPI [23]. O MPI vem evoluindo e também
podemos encontrar versões MPI2 [24] e OpenMPI [25], que é uma distribuição de
código aberto do MPI2.
Assim, o MPI possibilitou:

 Estandarização, pois o MPI fez que a passagem de mensagem se tornasse


um padrão, de tal forma que não seria mais necessário desenvolver programas
diferentes para cada tipo máquina existente.
 Portabilidade, programas MPI funcionam sobre multiprocessadores que ope-
ram no modelo de memória compartilhada, multicomputadores que operam no
modelo de memória distribuı́da, clusters de computadores, sistemas heterogé-
neos, etc.
 Ótimo desempenho, permite a exploração eficiente dos componentes compu-
tacionais desenvolvidos pelos fabricantes em implementações distintas para cad
equipamentos proprietário.
 Ampla funcionalidade, o MPI inclui grande quantidade de funções para
mostrar de uma maneira fácil as operações que habitualmente aparecem com
maior probabilidade em programas utilizando passagem de mensagem.

Neste curso estudaremos funções básicas de MPI e indicaremos as facilidades


que a biblioteca de programação oferece.

1.5.2. Conceitos básicos de MPI


Quando um programa MPI é inicializado, no mesmo instante de tempo vários
processos são criados e executados pelo mesmo código7 com suas próprias váriaveis.
A diferença básica em relação ao OpenMP é que não existe um processo destacado
(um thread mestre).
O algoritmo 1, código escrito em C, mostra uma versão básica de um código
tı́pico que imprime na tela ‘Alo Mundo” en MPI. Em que aparecem alguns de seus
componentes MPI:
7 Considerando o paradigma SPMD (Single Program Multiple Dada)
Algoritmo 1 Programa AloMundo.c com MPI
1 #include <stdio.h>
2 #include <mpi.h>
3

4 int main (int argc, char **argv){


5

6 /* Inicio de declaracao de variaveis */


7 int meu_id; // Id de cada processo
8 int numero_processos; // Qtd de processos
9 int tamanho_nome; // Comprimento do nome
10 char nome_Processador [MPI_MAX_PROCESSOR_NAME];// Nome host proc.
11 MPI_Status status; // Status de mesg MPI
12 /* Final de declaracao de variaveis */
13

14 MPI_Init(&argc,&argv); // Inicializando MPI


15 MPI_Comm_size(MPI_COMM_WORLD,&numero_processos); // Def. num de proc.
16 MPI_Comm_rank(MPI_COMM_WORLD,&meu_id); // Def. valor do Id
17 MPI_Get_processor_name(nome_Processador,&tamanho_nome);
18 printf("\nAlo Mundo!\n");
19 printf(" Eu sou o Host: %s.\n O número do processo que estou executando
20 é: %d \n No momento existe(m) %d processo(s) rodando.\n",
21 nome_Processador, meu_id, numero_processos);
22 MPI_Finalize();
23 }

 Na linha 02 verifica-se a inclusão da biblioteca (mpi.h).


 Todos os processos executam-se a partir de um mesmo código inicialmente, de
forma que todos tem um identificadorúnico meu_id. A diferença com OpenMP
é que estes identificadores podem estar em partes diferentes da memória em
processadores diferentes.
 Os processos trabalham de maneira independente até que se inicializa MPI
com a função MPI_Init. A partir desse ponto os processos podem colaborar
entre si, trocando dados, sincronizando-se, etc.
 A função MPI_Finalize tem como funcionalidade finalizar os processos inicia-
lizados anteriormente, liberando todos os recursos reservados pelo MPI.
 As funções MPI tem sempre a seguinte forma: MPI_Nombre(parámetros)
 As funções MPI_Comm_rank e MPI_Comm_size servem para identificar os pro-
cessos inicializados com um identificador( um número entre 0 e o número de
processos menos 1).
 Todas as funções tem como parâmetro MPI_COMM_WORLD, que é uma constante
MPI e que identifica o comunicador constituı́do por todos os processos. Um
comunicador é um identificador de um grupo de processos, e as funções MPI
tem que indicar em que comunicador se estão realizando as operações.

1.5.3. Compilação e Execução


Inicialmente veremos um primeiro exemplo bastante simples de como se com-
pila e se executa um código MPI. A forma de faze-lo pode variar conforme uma
implementação (versão) ou outra. Uma forma de compilar nas versões MPICH e
LAMMPI é utilizar o comando mpicc.Este comando é usado para compilar e ”lin-
kar”programas MPI escritos em C. Provê as opções e quaisquer bibliotecas especiais
necessárias para compilar e ”linkar”programas MPI.
mpicc AloMundo.c -o AloMundo
Desta forma, o comando mpicc chama o compilador C e realiza a linkagem com a
biblioteca MPI e código fonte.
Uma vez gerado o código (no exemplo acima, o algoritmo 1), a forma de
executa-lo também depende da compilação. O normal é chamar o comando mpirun
passando-lhe o código a ser executado e uma serie de argumentos que indicam os
processos a serem inicializados e a distribuição dos processos nos processadores, na
seguinte forma:
mpirun -np 4 AloMundo
No caso acima estamos solicitando que sejam executados 4 processos (-np 4) todos
executando o mesmo código (AloMundo). Os 4 processos inicializados podem estar
atribuı́dos ao mesmo processador ou não. O fator decisivo que irá determinar quais
máquinas serão utilizadas como hospedeiras dos processos recém criados será a in-
clusão de um arquivo de especificação de mo ’aquinas junto a linha de execução do
processo, por exemplo:
mpirun -np 4 -machinefile maquinas.conf AloMundo
Nesse arquivo de máquinas (maquinas.conf) serão indicados os nomes dos hosts que
irão participar na execução dos processos assim como a quantidade de processos
serão inicializados em cada nodo. Um exemplo de um arquivo de máquinas:
host 01 3
host 02 1
host 03 1
host 04 2

Caso seja iniciada a execução (i.e., execução do comando mpirun) a partir do host 01
os 4 processos seriam atribuı́dos as máquinas seguindo a ordem do arquivo de má-
quinas e sua configuração. Neste caso 3 processos no host 01 e 1 processo no host 02.
A forma de atribuição desses processos segue uma ordem cı́clica padrão do MPI.
Também é possı́vel lançar a execução especificando os processos por linha de
comando sendo argumentos do mpirun
mpirun n0,1,2,3 AloMundo
onde está sendo lançado 4 processos que são atribuı́dos aos hosts 0, 1, 2 e 3. Esta
forma de trabalhar é tı́pica do LAMMPI. Outra diferença no lançamento dos pro-
cessos MPI utilizando a versão LAMMPI, é que antes de inicializarmos os processos
temos que formar uma rede de execução através do comando lamboot e somente
depois executamos o mpirun.

1.5.4. Comunicações ponto a ponto


No exemplo anterior (AloMundo.c) os processos não interagem entre si. O
normal é que os processos não trabalhem de maneira independente, mas que troquem
informações por meio do passagem de mensagem. Para enviar mensagens entre dos
processos (um de origem e outro de destino) utilizamos as as comunicações ponto
a ponto. O algoritmo 2, código EnviaRecebe.c mostra a impressão em tela de uma
mensagem texto “Sou Processo X, recebi o valor Y” com este tipo de comunicações.
São utilizadas funções MPI_Send e MPI_Recv para enviar e receber mensagens.
A forma das funções pode ser resumida da seguinte forma:
int MPI_Send(void *buf, int count,
MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
int MPI_Recv(void *buf, int count,
MPI_Datatype datatype, int source, int tag, MPI_Comm comm,
MPI_Status *status)
Onde os parámetros possuem o seguinte significado:

 buf contém o inicio da zona de memória do dado a ser enviado ou onde vai ser
armazenado em sua recepção. count contém o número de dados a ser enviado
ou o espaço disponı́vel para receber.

 datatype é o tipo de dado a transferir, que sao tipos padrões do MPI


(MPI_Datatype), em nosso exemplo o tipo é MPI_INT.

 dest e source sao identificadores do processo a quem se envia e de quem recebe


a mensagem. Se pode utilizar a constante MPI_ANY_SOURCE para indicar que
se recebe de qualquer origem.

 O parâmetro tag se utiliza para diferenciar as mensagens. No exemplo nos


dois casos possui o valor 0. Podemos utilizar MPI_ANY_TAG para indicar que a
mensagem é compatı́vel com mensagem de qualquer identificador.

 comm é o comunicador, é o ambiente responsável pela comunicação. é do tipo


MPI MPI_Comm, no exemplo se usa o identificador do comunicador formado por
todos os processos (MPI COMM WORLD).

 status referencia uma variável do tipo MPI_Status. Contém informações da


mensagem que se recebeu, e pode ser consultada para identificar alguma ca-
racterı́stica da mensagem, por exemplo sua longitude, o processo de origem,
etc.
Algoritmo 2 Programa EnviaRecebe.c com MPI
1 #include <stdio.h>
2 #include <mpi.h>
3 int main (int argc, char **argv){
4 int meu_id, tamanho_nome, contador;
5 int numero_processos, origem, destino, mensagem=0, tag=0;
6 char nome_host [MPI_MAX_PROCESSOR_NAME];
7 MPI_Status status;
8 MPI_Init(&argc, &argv); /*Inicializa MPI */
9 MPI_Comm_rank(MPI_COMM_WORLD,&meu_id); /*Id do Proc. */
10 MPI_Comm_size(MPI_COMM_WORLD,&numero_processos); /*Total de Proc. */
11 MPI_Get_processor_name(nome_host,&tamanho_nome); /*Descobre o nome*/
12 if (meu_id==0) { /*processo com id zero*/
13 destino=1;
14 printf("\n Sou o processo $s, [%d]\n", nome_host,meu_id);
15 MPI_Send(meu_id,1,MPI_INT,destino,tag,MPI_COMM_WORLD);
16 MPI_Recv(mensagem,1,MPI_INT,destino,tag,MPI_COMM_WORLD,&status);
17 printf("Recebi valor %d do proc. %d \n\n",mensagem,destino);
18 }
19 else {
20 MPI_Recv(mensagem,1,MPI_INT,MPI_ANY_SOURCE,tag,MPI_COMM_WORLD,&status);
21 origem = status.MPI_SOURCE;
22 mensagem=(mensagem*3);
23 MPI_Send(mensagem,1,MPI_INT,origem,tag,MPI_COMM_WORLD);
24 }
25 MPI_Finalize();
26 return(0);
27 }

O programa mostra a forma que, normalmente, se trabalha com passagem de


mensagens. O mesmo programa executa em todos os processadores, mas processos
diferentes executam partes diferentes do código: o processo 0 Envia seu ID e recebe
um inteiro como resultado de uma multiplicação por 3, realizada pelo processo 1.
O algoritmo 3, código Pi.c mostra uma versão MPI para o problema da
integração numérica do número π. Cada processo calcula parte da integral, determi-
nando os retângulos a calcular por meio do identificador do processo e do número de
processos. Finalmente o processo 0 recebe dos demais, as áreas parciais e as acumula.
Este código MPI é mais complicado que o mesmo feito em OpenMP, devido que se
tem que incluir as comunicações necessárias para acessar os dados entre os processos,
que não era necessário em OpenMP por estar os dados em memoria compartilhada.
Algoritmo 3 Programa Pi.c com MPI
1 #include <mpi.h>
2 #include <stdio.h>
3 int main( int argc, char **argv) {
4 int n, myid, numprocs, i;
5 double PI25DT = 3.141592653589793238462643, mypi, pi, h, sum, x;
6 MPI_Init(&argc,&argv);
7 MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
8 MPI_Comm_rank(MPI_COMM_WORLD,&myid);
9 while (1) {
10 if (myid == 0){
11 printf("Numero de Intervalos: (0 quits) ");
12 scanf("%d",&n);
13 }
14 MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
15 if (n == 0) break;
16 else {
17 h = 1.0 / (double) n;
18 sum = 0.0;
19 for (i = myid + 1; i <= n; i += numprocs) {
20 x = h * ((double)i - 0.5);
21 sum += (4.0 / (1.0 + x*x));
22 }
23 mypi = h * sum;
24 MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
25 if (myid == 0)
26 printf("pi: %.16f, Erro: %.16f\n", pi, fabs(pi - PI25DT));
27 }
28 }
29 MPI_Finalize();
30 return 0;
31 }

As comunicações que são estabelecidas no codigo3-6.c (MPI_Send e MPI_Recv)


são chamadas de bloqueantes, pois bloqueiam o processo emissor/receptor até
que a mensagem seja totalmente entregue/recebida. MPI ainda proporciona ou-
tras possibilidades para a troca de mensagens no formato bloqueante, por exemplo
MPI_Ssend, MPI_Bsend y MPI_Rsend
Estas funções se diferenciam na forma de gestionar o envio, fazendo com que
o processo acesse o dado diretamente no buffer da memória.
A função MPI_Sendrecv combina as chamadas de envio e de recepção entre
dois processos, sendo essa função bloqueante.
MPI também proporciona comunicação não bloqueante, em que o processo
receptor solicita a mensagem e mesmo que sua chegada não for confirmada, continua
com seu fluxo normal de execução (e.g., MPI_Isend e MPI_Irecv). Caso o processo
receptor não tenha recebido a mensagem pode chegar um momento em que de forma
obrigatória, tenha que esperar o recebimento para poder continuar sua execução
(e.g., dependencia de dados). Existem duas funções para isto: MPI_Wait para esperar
a chegada de uma mensagem e MPI_Test para comprovar se a operação completou
com sucesso.
Como prática podemos modificar codigo3-7.c para utilizar comunicações
ası́cronas de tal forma que o processo 0 não receba as mensagens na ordem preesta-
belecida.
Os tipos de dados utilizados pelas mensagens em MPI:

Tipo MPI Tipo C


MPI_CHAR signed char
MPI_SHORT signed short int
MPI_INT signed int
MPI_LONG signed long int
MPI_UNSIGNED_CHAR unsigned char
MPI_UNSIGNED_SHORT unsigned short int
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long int
MPI_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double

1.5.5. Comunicações coletivas


Além das comunicações ponto a ponto, MPI oferece uma serie de funções
que possibilitam comunicações em que intervém todos os processos e um comunica-
dor, sempre que seja possı́vel realizar as comunicações por meio das comunicações
coletivas.
O problema codigo3-7.c os processos inicializados, uma vez calculada área
locais as enviariam ao processo 0, que as acumula para obter o valor final da integral.
Esta comunicação pode ser feita com a função MPI_Reduce. No código codigo3-8.c
é mostrado uma versão do calculo de π usando comunicações coletivas. Também
é possı́vel utilizar a função MPI_Bcast para enviar desde o processo 0 aos demais
processos utilizados na integração. Esta é uma forma normal de se trabalhar com
passagem de mensagem: um processo diferenciado dos demais (geralmente se utiliza
o 0) realiza a entrada dos dados, os distribui ao resto dos processos, todos intervém
na computação e finalmente a saı́da dos resultados realiza o processo diferenciado.
Enumeramos algumas das comunicações coletivas mais utilizadas:
 int MPI_Barrier(MPI_Comm comm) estabelece uma barreira. Todos os proces-
sos esperam a que todos cheguem a barreira, para continuar a execução quando
todos tenham chegado.

 int MPI_Bcast(void *buffer, int count,


MPI_Datatype datatype, int root, MPI_Comm comm) realiza uma operação
de broadcast (comunicação de um a todos), onde são mandados count dados
do tipo datatype desde o processo raı́z (root) ao resto dos processos no co-
municador.

 int MPI_Reduce(void *sendbuf, void *recvbuf, int count,


MPI_Datatype datatype, MPI_Op op, int root,
MPI_Comm comm) realiza uma redução de todos para um. O resultado é ar-
mazenado no processo root. A operação que se aplica aos dados vem indicada
por op. Os tipos de operações que são admitidos provem na seguinte tabela:

Operação Significado Tipos permitidos


MPI_MAX máximo Inteiros e ponto flutuante
MPI_MIN mı́nimo Inteiros e ponto flutuante
MPI_SUM soma Inteiros e ponto flutuante
MPI_PROD produto Inteiros e ponto flutuante
MPI_LAND AND lógico Inteiros
MPI_LOR OR lógico Inteiros
MPI_LXOR XOR lógico Inteiros
MPI_BAND bitwise AND Inteiros e Bytes
MPI_BOR bitwise OR Inteiros e Bytes
MPI_BXOR bitwise XOR Inteiros e Bytes
MPI_MAXLOC máximo e localização Pares do tipos
MPI_MINLOC mı́nimo e localização Pares do tipos

 Quando todos os processos tem que receber o resultado da operação é utilizada


a função int MPI_Allreduce(void *sendbuf,void *recvbuf, int count,
MPI_Datatype datatype, MPI_Op op, MPI_Comm comm).

 Para enviar desde um processo, mensagens diferentes ao resto dos processos,


pode-se utilizar int MPI_Scatter (void *sendbuf, int sendcount,
MPI_Datatype sendtype, void *recvbuf, int recvcount,
MPI_Datatype recvtype, int root, MPI_Comm comm). O processo raı́z da
mensagem é divido em segmentos de tamanho sendcount, e o segmento i-
ésimo é enviado ao processo i.

 A função inversa da função MPI_Scatter é int MPI_Gather(


void *sendbuf, int sendcount, MPI_Datatype sendtype,
void *recvbuf, int recvcount,MPI_Datatype recvtype, int root, MPI_Comm
comm). Todos os processos (incluindo o raı́z) enviam ao processo raı́z sendcout
dados de sendbuf, e o raı́z os armazena em recvbuf pela ordem dos processos.
 Para mandar blocos de dados de todos a todos os processos utiliza-se
int MPI_Allgather (void *sendbuf, int sendcount,
MPI_Datatype sendtype, void *recvbuf, int recvcount,
MPI_Datatype recvtype, MPI_Comm comm), onde o bloco enviado pelo i-ésimo
processo é armazanado como bloco i-ésimo em recvbuf todos os processos. Pa-
ra enviar blocos de tamanhos diferentes utiliza-se MPI_Allgatherv.

 Para mandar blocos de dados diferentes aos diferentes processos deve-se utilizar
int MPI_Alltoall(void *sendbuf, int sendcount,
MPI_Datatype sendtype, void *recvbuf, int recvcount,
MPI_Datatype recvtype, MPI_Comm comm). Para cada processo i, o
bloco j será enviado ao processo j, que o armazena como bloco i em recvbuf.

1.5.6. Comunicadores e topologias


Até agora somente mencionamos algumas das 120 funções que o MPI possui.
De todas elas sao imprescindı́veis as de inicializar e finalizar o ambiente, as de ob-
ter o número de processos, as de identificação de processos e as funções de envio e
recebimento de mensagens. O resto das funções estão orientadas a facilitar o desen-
volvimento de programas ou torna-los mais eficientes. Não podemos em um curso
de inicialização estudar com detalhes todas as funcionalidades que oferece o MPI,
a idéia desse tipo de aprendizado é comentar as noções básicas de comunicadores e
topologias.
Vimos nos programas anteriores que nas funções de comunicação é utilizada
a constante MPI_COMM_WORLD. Esta constante identifica um comunicador que inclui
a todos os processos. Um comunicador define uma série de processos entre os que
podem realizar comunicações. Cada processo pode estar em vários comunicadores
e terão um identificador de cada um deles, estando os identificadores entre 0 e o
número de processos do comunicador menos 1.
Existe dois tipos de comunicadores: Os intracomunicadores que se utilizam
para enviar mensagens entre os processos nesse comunicador, e os intercomunicadores
que se utiliza para enviar mensagens entre distintos comunicadores. Em nosso exem-
plo as comunicações serão sempre entre processos e um mesmo comunicador. As
comunicações entre processos em comunicadores diferentes podem ter sentido si fo-
rem desenhadas bibliotecas que criam comunicadores e comunicam um processo do
seu programa com outro pertencente a outra biblioteca.
Um comunicador é constituı́do de: um grupo, que é uma coleção ordenada
de processos aos que se quer associar identificadores, e um contexto que é um iden-
tificador que associa o sistema a um grupo. Ao mesmo tempo que um comunicador
pode lhe associar a uma topologia virtual.
Se supomos que temos os processos em uma malha virtual, com p = q2
processos agrupados em q linhas y colunas, e que o processo x tem coordenadas
(x div q, x mod q), para criar um comunicador para a primeira linha de processos se
faria da seguinte forma:
//Declara-se o grupo associado ao comunicador de todos os processos
MPI_Group MPI_GROUP_WORLD;
//Declara- o grupo e o comunicador que será criado
MPI_Group first_row_group;
MPI_Comm first_row_comm;

//Armazena-se os identificadores de processos os quais se incluem no comunicador


int *process_ranks;
process_ranks=(int *) malloc(q*sizeof(int));
for(proc=0;proc<q;proc++)
process_ranks[proc]=proc;
MPI_Comm_group(MPI_COMM_WORLD,&MPI_GROUP_WORLD);
MPI_Group_incl(MPI_GROUP_WORLD,q,process_ranks,
&first_row_group);
MPI_Comm_create(MPI_COMM_WORLD,first_row_group,
&first_row_comm);
MPI_Comm_group y MPI_Group_incl sao locais e não existem comunicações,
y MPI_Comm_create é uma operação coletiva, e todos os processos do comunicador
onde se está trabalhando devem executar-la ainda que não vá formar parte do novo
grupo.
Para se criar vários comunicadores disjuntos se pode usar a função int
MPI_Comm_split( MPI_Comm old_comm, int split_key,
int rank_key, MPI_Comm *new_comm), que se cria um novo comunicador para ca-
da valor de split_key, formando parte do mesmo grupo d processos com o mesmo
valor. Se dois processos a e b tem o mesmo valor de split_key e o rank_key de a é
menor do que o de b, em um novo grupo a tem identificador menor que b, e se os dois
tem o mesmo rank_key o sistema atribui os identificadores arbitrariamente. Esta
função é uma operação coletiva, no qual todos os processos no comunicador devem
chama-la. OS processos que não incluem em nenhum novo comunicador utilizam o
valor MPI_UNDEFINED em split_key, com que o valor de retorno de new_comm é
MPI_COMM_NULL.
Se consideramos uma malha lógica de processos como antes, se podem criar
q grupos de processos associados as q filas:
MPI_Comm my_row_comm;
int my_row=my_rank/q;
MPI_Comm_split(MPI_COMM_WORLD,my_row,my_rank,&my_row_comm);
Em MPI pode ser associar uma topologia a un grupo de processos. Uma
topologia descreve como se comunicam os processos entre si, e sao topologias lógicas
o virtuais, que se usam para descrever o padrão de comunicações que nos interessa
usar em nosso programa, ou também para facilitar o mapeio dos processos en um
sistema fı́sico sobre o que se vai executar.
Se podem associar topologias de grafo em geral ou de malha ou cartesiana.
Una topologia cartesiana se cria con int MPI_Card_create( MPI_Comm old_comm,
int number_of_dims, int *dim_sizes, int *periods,int reorder, MPI_Comm
*cart_comm), onde o número de dimensões da malha é number_of_dims, o número
de processos em cada dimensão está en
dim_sizes, com periods se indica se cada dimensão é circular ou linear, e o valor 1
em reorder indica ao sistema que se reordenem os processos para otimizar a relação
entre o sistema fı́sico e o lógico.
Em uma topologia de malha se pode obter as coordenadas de un processo
conhecido por seu identificador con int MPI_Cart_coords( MPI_Comm comm, int
rank, int number_of_dims, int *coordinates), e o identificador conhecido as
coordenadas com int MPI_Cart_rank( MPI_Comm comm, int *coordinates, int
*rank).
Uma malha pode se particionar em malhas de menor dimensão con int
MPI_Cart_sub( MPI_Comm old_comm, int *varying_coords,
MPI_Comm *new_comm), donde en
varying_coords se indica para cada dimensão se pertence ao novo comunicador.
Por exemplo, se varying_coords[0]=0 e varying_coords[1]=1, para obter o novo
comunicador não se varia a primeira dimensão mas se a segunda, com que se cria
um comunicador por cada fila.

1.6. Notas e Referências Bibliográficas


Os conceitos relacionados com os modelos de programação em paralelo fo-
ram sido tratados desde diferentes perspectivas por vários autores. As referências
mais importantes dosúltimos anos podemos encontrar nos livros de Grama, Gup-
ta, Karypis y Kumar [26], que abordam uma variedade de exemplos e exercı́cios
práticos.
Abordam também aspectos relativos a programação com MPI utilizando pas-
sagem de mensagem, OpenMP e bibliotecas de threads para o modelo de memória
compartilhada. Podemos mencionar os livros de Wilkinson e Allen [27] e o de Quinn
[28]. E no que se refere a aspectos de programação mediante MPI, destaca-se o livro
[29] por sua caracterı́stica didática que junto com o livro [11] e a própria descrição
do padrão do MPI em [4], constituem uma bibliografia completa sobre o tema.
No que se refere a programação no modelo de memória compartilhada com
OpenMP o texto [7] e o livro Rodrı́guez [30] (em formato eletrônico) junto com a
interface [8], também abordam uma boa visão do tema.
Referências Bibliográficas
[1] Bob Greson, Angela Burgess, and Christine Miler. Timeline of Computing
History, http://www.computer.org/computer/timeline/timeline.pdf.

[2] Francisco Almeida, Domingo Giménez, José Miguel Mantas, and Antonio M.
Vidal. Introducción a la programación paralela. Paraninfo Cengage Learning,
2008.

[3] OpenMP. http://www.openmp.org/blog/.

[4] Message Passing Interface Forum. MPI: A Message Passing Interface Standard.
Univ. of Tennessee, Knoxville, Tennessee, 1995.

[5] Bradford Nichols, Dick Buttlar, and Jacqueline Proulx Farrel. Pthreads pro-
gramming: A Posix Standard for Better Multiprocessing. O’Reilly, 1996.

[6] Threading Building Blocks. http://www.threadingbuildingblocks.org/.

[7] Rohit Chandra, Ramesh Menon, Leo Dagum, David Kohr, Dror Maydan, and
Jeff McDonald. Parallel Programming in OpenMP. Morgan Kauffman, 2001.

[8] Rohit Chandra, Ramesh Menon, Leo Dagum, David Kohr, Dror Maydan,
and Jeff McDonald. OpenMP C and C++ Application Program Interfa-
ce. OpenMP Architecture Review Board. http://www.openmp.org/drupal/mp-
documents/cspec20.pdf, 2002.

[9] A. Geist, A. Beguelin, J. J. Dongarra, W. Jiang, R. Manchek, and V. Sunderam.


PVM 3.0 User’s Guide and Reference Manual. Technical Report ORNL/TM-
12187, Mathematical Sciences Section, Oak Ridge National Laboratory, 1996.

[10] BSP. http://www.bsp-worldwide.org/.

[11] Marc Snir and William Gropp. MPI. The Complete Reference. 2nd edition.
The MIT Press, 1998.

[12] Kai Hwang. Advanced Computer Architecture: Parallelism, Scalability, Pro-


grammability, 1st edition. McGraw-Hill, 1992.

[13] Hennessy. Computer architecture: a quantitative approach, 3rd ed. Morgan


Kauffman, 2003.

[14] J. Ortega, M. Anguita, and A. Prieto. Arquitectura de Computadores. Thomson,


2004.

[15] Gordon Moore. Cramming more components onto integrated circuits. Electro-
nics Magazine, 1965.

[16] Robert R. Schaller. Moore’s law: past, present, and future. IEEE Spectrum,
34:52–59, 1997.
[17] M. J. Flynn. Some computer organizations and their effectivness. IEEE Tran-
sactions on Computers, 21:948–960, 1972.

[18] Selim G. Akl. Diseño y análisis de algoritmos paralelos. Ra-Ma, 1992.

[19] FT-MPI. http://icl.cs.utk.edu/ftmpi/.

[20] HeteroMPI. http://hcl.ucd.ie/profile/HeteroMPI.

[21] MPICH-Madeleine. http://runtime.bordeaux.inria.fr/mpi/.

[22] MPICH. http://www-unix.mcs.anl.gov/mpi/mpich1/.

[23] LAM-MPI. http://www.lam-mpi.org/.

[24] MPI Forum. http://www.mpi-forum.org/.

[25] OpenMPI. http://www.open-mpi.org/.

[26] A. Grama, A. Gupta, G. Karypis, and V. Kumar. Introduction to Parallel


Computing. Addison-Wesley, second edition, 2003.

[27] Barry Wilkinson and Michael Allen. Parallel Programming: Techniques and
Applications Using Networked Workstations and Parallel Computers. Prentice-
Hall, second edition, 2005.

[28] Michael J. Quinn. Parallel Programming in C with MPI and OpenMP. McGraw
Hill, 2004.

[29] Peter Pacheco. Parallel Programming with MPI. Morgan Kaufmann Publishers,
1997.

[30] Casiano Rodrı́guez León. The Design, Analysis and Imple-


mentation of Algorithms for Parallel Shared Memory Machines.
http://nereida.deioc.ull.es/ pp1/openmp/openmpbook.ps, 2002.

Você também pode gostar