práticas em programação paralela - lbd.dcc.ufmg.br · existem duas classes distintas de sistemas...

37

Upload: vuliem

Post on 20-Nov-2018

213 views

Category:

Documents


0 download

TRANSCRIPT

Práticas em Programação Paralela1

Liria M atsumoto Sato

Edson Toshimi Midorikawa

Volnys Borges Berna/

Laboratório de Sistemas Integráveis

Escola Politécnica da Universidade de São Paulo

Av. Prof. Luciano Gualberto, Travessa 3, n2158

05508-900

São Paulo, SP

Tel: (011) 815-9322 R.3270/3589

e-mail: { /iria, emidorik, volnys} @lsi.usp.br

RESUMO

Dada a construção de diversas máquinas com arquitetura paralela e a complexidade de elaborar

programas que utilizem com eficiência os seus recursos de paralelismo, ambientes de programação

adequados a essa classe de computadores são necessários. Este trabalho aborda as linhas atuais nesta

área. São apresentados os modelos de programação paralela e as formas existentes para a exploração

das fontes de paralelismo presentes em um programa.

ABSTRACT

Due to the construction of several machines with parallel archüecture and the complexity of

developing programs that efficiently use their parallel capabilities, programming environments

adequate for this class of computers are necessary. This paper describes current work in this area.

Parallel programming models are presented as well as existing forms of exploring sources of

parallelism present in a program.

TERMOS-CHAVE: programação paralela, modelos de paralelismo, dependência de dados, paralelismo explícito, paralelismo implícito, paralelizadores automáticos, linguagens de programação paralela.

1. Este trabalho é urna versão revista e ampliada de [1 O] .

4

1. Introdução

Com a crescente demanda de processamento em aplicações em diversas áreas, incluindo desde a engenharia e a física até a medicina e a biologia, novas máquinas que buscam proporcionar

desempenhos cada vez maiores estão surgindo.

Para se obter alta capacidade de processamento, uma alternativa é utilizar processadores potentes. Outra alternativa é paralelizar a execução das operações. Operações podem ser paralelizadas ao nível do processador, onde as fases de cada instrução são "pipelined", e ao nível de instruções, onde estas são executadas simultaneamente por múltiplos processadores. A utilização de múltiplos processadores em computadores pode promover ganhos significativos de desempenho.

Existem duas classes distintas de sistemas com múltiplos processadores: os multiprocessadores com memória compartilhada, que possuem uma unidade de memória acessível por todos os processadores, e os multiprocessadores com memória distribuída, onde os processadores têm apenas acesso às suas respectivas memórias locais (fig.l ). As técnicas de paralelização empregadas nestas duas classes são diferentes2. Neste trabalho, abordaremos os aspectos de programação para a classe dos multiprocessadores com memória compartilhada.

Rede de Interconexão

Memória Global

(a) Multiprocessador com memória compartilhada. (b) Multiprocessador com memória distribufda.

Figura 1 - Arquiteturas de sistemas multiprocessadores.

É fato que o aproveitamento dos recursos de processamento da máquina exige ferramentas de programação que permitam explorar ao máximo o paralelismo presente.

Em sistemas multiprocessadores, são duas as formas para se utilizar os múltiplos processadores disponíveis. A primeira é explicitar no programa o paralelismo. E a segunda, é utilizar compiladores paralelizantes que exploram o paralelismo implícito em programas escritos em linguagem de programação seqtlencial.

Ambas apresentam vantagens e desvantagens e são atualmente temas de grande interesse na busca de se obter o máximo aproveitamento dos recursos oferecidos pelos sistemas com arquitetura paralela.

A experiência tem mostrado que a programação paralela é significativamente mais difícil que a programação sequencial, principalmente porque envolve tanto a necessidade da sincronização entre 2. para maiores informações, consulte [2], [5], [13] e [14].

5

tarefas, como também a análise de dependência de dados. Estas dificuldades são evitadas quando se utiliza um sistema paralelizante. Por outro lado, a disponibilidade de construções para explicitar o paralelismo oferece ao usuário uma forma de particionar o seu programa em tarefas simultâneas, o que em algumas aplicações, tal como simulações, é de grande utilidade.

Neste trabalho são apresentados os modelos de programação paralela existentes e as formas para a exploraç!lo das fontes de paralelismo presentes em um programa.

2. Paradigmas de Programação Paralela

A paralelizaç!lo do algoritmo em termos de blocos de instruções executadas paralelamente pode ser classificada em:

• homogênea e

• heterogênea.

2.1 - Paralelização homogênea

A paralelizaç!lo homogênea (fig.2) envolve a execução paralela do mesmo código sobre elementos múltiplos de dados. Esta paralelização é efetuada em "loops", devendo para tanto serrealizada uma análise de dependência de dados no seu código. A execução de cada iteração deve ser completamente independente de qualquer outra. O paralelismo homogêneo é facilmente reconhecido pelos analisadores que identificam e analisam os "loops" presentes no programa.

Figura 2 - Paralelização homogênea.

2.2 - Paralelização heterogênea

A paralelização heterogênea (fig.3) envolve a execução paralela de códigos múltiplos sobre dados múltiplos. Esta paralelização é possível quando a tarefa a ser realizada pelo algoritmo pode ser particionada em múltiplas subtarefas, sendo que cada uma, em um ambiente paralelo, poderia ser executada por um processador.

Figura 3 - Paralelização heterogênea.

6

3. Granularidade

A granularidade reflete a distribuiç!o do trabalho do algoritmo em subtarefas independentes executadas paralelamente. A granularidade pode ser

• fina ("fine") ou

• grossa ("coarse").

Uma medida de granularidade é a quantidade de processamento envolvido em uma subtarefa. Por exemplo se uma subtarefa faz uma única atribuiç!o ao dado pode ser considerada como de granularidade fina. Já se envolve a execução de múltiplas atribuições ou mesmo chamadas de subrotinas pode ser considerada de granularidade grossa.

Cabe ressaltar que devido ao custo inerente da paralelização, como por exemplo o custo referente à sincronizaç!o, a granulação influi no desempenho do algoritmo.

4. Modelos de Programação Paralela

Considerando os métodos de paralelizaç!o e o fator da granularidade pode- se classificar quatro paradigmas de programação paralela principais [1] .

4.1 - Paralelismo Homogêneo de granulação fina

Envolve a execução de um mesmo código que contém pouca manipulação do dado sobre múltiplos elementos de dados.

Exemplo:

4.2 - Paralelismo Homogêneo de granulação grossa

Envolve a execução de um mesmo código que contém por exemplo múltiplas atribuições ou mesmo chamadas de subrotinas sobre múltiplos elementos de dados.

Exemplo:

7

4.3 - Paralelismo Heterogêneo de granulação fina

Envolve a execução de códigos distintos com pouca manipulação do dado sobre múltiplos dados.

Exemplo:

4.4 - Paralelismo Heterogêneo de granulação grossa

Envolve a execução de códigos distintos com muita manipulação do dado realizando múltiplas atribuições ou até mesmo chamadas de subrotinas sobre múltiplos dados.

Exemplo:

4.5 - O Programa Paralelo

Os modelos apresentados anteriormente se referem a blocos paralelizados. Considerando um programa, este pode utilizar uma combinação destes modelos. Assim, em um mesmo programa, podemos encontrar diversos blocos com granulações e paradigmas diferentes, ou seja, adotando os diversos modelos de programação paralela.

5. Programação Paralela Explícita

Uma das formas existentes para a exploração dos recursos de paralelismo em um sistema multiprocessador é explicitar manualmente as porções de código paralelo de um programa. A programação paralela explícita pode ser realizada das seguintes formas:

8

• por chamadas de rotinas de uma biblioteca que trata os recursos de paralelismo;

• através de diretivas do compilador (pragmas); e

• através de construções paralelas oferecidas por linguagens de programação paralela.

5.1 - Biblioteca de paralelismo

Alguns sistemas permitem ao usuário especificar o paralelismo em seu programa através de chamadas de rotinas de uma biblioteca que implementa o paralelismo.

Será mostrado como programar utilizando uma biblioteca de paralelismo tomando como exemplo

o sistema Sequent.

Sistemas Sequent são multiprocessadores que incorporam múltiplos processadores idênticos e uma tínica memória comum. Suportam dois tipos de programação paralela: multiprogramação, que permite ao computador executar concorrentemente programas não relacionados, e "multitasking", que permite que uma única aplicação consista de múltiplos processos executando

concorrentemente.

Muitas aplicações se orientam naturalmente para um dos seguintes métodos de programação: paralelismo homogêneo, também referenciado como particionamento de dados, e paralelismo heterogêneo ao nível de subprogramas, também denominado particionamento de funções.

O particionamento de dados é algumas vezes chamado de "microtasking". Programas "microtasking" criam múltiplos processos independentes para executar iterações de "loop" em paralelo. O corpo do "loop'' para ser executado em paralelo deve estar contido em um subprograma.

Computadores Sequent processam sobre o sistema operacional DINIX, uma versão do UNIX 4.2BSD. A "DYNIX Parallel Programming Library" é um conjunto de rotinas que permitem ao programador executar subprogramas C, Fortran ou Pascal em paralelo. A biblioteca inclui rotinas

para manipular as seguintes funções:

• alocação de memória para dados compartilhados;

• criação de processos para executar subprogramas em paralelo;

• identificação de processos individual;

• suspensão de processos durante seções seriais do programa;

• exclusão mútua sobre dado compartilhado; e

• sincronização de processos.

A "DYNIX Parallel Programming Library" [4] inclui três conjuntos de rotinas: uma biblioteca de "microtask:ing", um conjunto de rotinas para uso geral em programas com particionamento de dados, e um conjunto de rotinas para alocação de memória em programas com particionamento de dados.

O Exemplo 1 abaixo mostra um programa utilizando as rotinas da "DYNIX Parallel Programming Library".

Exemplo 1 :

9

10

5.2 - Especificação de paralelismo através de diretivas do compilador

Para mostrar como explicitar o paralelismo utilizando diretivas será tomado como exemplo o compilador IRIS Power C instalado em um computador multiprocessador com memória compartilhada.

A paralelização no IRIS Power C é introduzida no código C pelo uso de pragmas do compilador. Pragmas do compilador são um mecanismo adicionado na ANSI C para incluir diretivas no código. Não mudam o significado semântico do código.

O formato geral do pragma é como segue:

tpragma diretiva [(rnodificador(lista ... )]

Exemplos:

fpragma parallel f pragrna pfor iterate(i-O;rnaxits;l)

O exemplo 2 mostra um programa que utiliza pragmas para paralelizar.

Exemplo 2:

11

12

Passamos agora a descrever com maior detalhe como o paralelismo na IRIS Power C pode ser implementado em seções do programa denominadas regiões paralelas. [1] [11]

5.2.1 - Regiões paralelas

O programa começa no modo serial. Quando é atingida uma região paralela, "threads" são criados e executados concorrentemente. Finalizando a região paralela, os "threads" sincronizam-se e somente um "thread", representando o programa principal, continua a execução no modo serial (fig.4).

Processador

3

2

1

o

Início Final

SeqUencial Paralelo SeqUencial Paralelo SeqUencial

Figura 4 - Região paralela.

Uma região paralela é especificada por:

fpragma parallel

Este pragma é importante pois todas as demais construções paralelas são definidas dentro de regiões paralelas.

Exemplo:

tpragma par allel {

código bloco A

5.2.2 - Código local

O código local é executado localmente por todos os threads.

Exemplo:

tpragma parallel {

código local 1

13

Considerando 4 threads para a execução da região paralela tem-se: (fig.5)

Thread

3 c6d. local

2 c6d. local

c6d. local

o c6d. local

Figura 5 - Região paralela.

5.2.3 - Pragmas modificadores

O comportamento das variáveis na região paralela é definido por pragmas modificadores. Alguns modificadores agem na execução do programa. São eles:

tpragrna shared(lista de variáveis) tpragrna byvalue(lista de variáveis) tpragrna local(lista de variáveis) #pragrna if(express~o inteira ou valor) fpragrna [no] ifinline #pragrna numthreads(valor)

Onde temos que

+ shared: define variáveis compartilhadas por todos os threads.

+ byvalue: define variáveis cujos valores são compartilhados. Variáveis que são apenas lidas devem ser declaradas por "byvalue".

+ local: definem variáveis que são locais a cada thread.

+ i f: realiza um teste no tempo de execução que seleciona entre a execução serial e paralela dependendo das condições. Se a expressão ou valor for diferente de zero a região paralela é executada paralelamente, caso contrário é executada serialmente.

+ [no] ifinline: é usada em conjunto com o modificador #if para melhorar o desempemho.

+ numthreads: define o número de threads disponíveis para a execução da região paralela.

Exemplo:

fpragrna parallel #pragrna s hared(a,b) tpragrna local(i,j) #pragrna numthreads(2) {

código

14

Vários modificadores podem ser especificados em uma mesma linha:

tpragma shared(a,b) local(i,j)

5.2.4 - Loops

Um "for loop" em uma região paralela pode ser executado paralelamente, utilizando- se:

Exemplo:

tpragma pfor

tpragma parallel tpragma shared(a,b,c) tpragma byvalue(maxits) tpragma local(i) {

tpragma pfor i terate(i=O;maxits;l) for (i=O;i<maxits;i++)

a[i]=b[i]+c[i];

5.2.5 - Modificadores pfor

O pragma pfor tem vários modificadores que definem o comportamento do "loop" paralelo. São:

Onde

iterate(index=start ;total_iterações ;passo) schedtype(type ) chunksize(valor)

• iterate: inicia index, que deve ser do tipo local, define o total de iterações e o tamanho do passo.

• schedtype: define como o compilador divide as iterações entre os "threads", através de "type". Os tipos disponfveis são:

O simple (default): o número de iterações é dividido igualmente entre todos os

threads.

O runtime: a escolha do escalonamento é detenninado em tempo de execução pela

"envirorunent variable" MPC_SCHEDTYPE.

O dynamic: a cada "thread" é dinamicamente dado um bloco de iterações cujo

tamanho é defirrido por #chunksize(valor).

O interleave: a cada "thread" é atribuído um bloco de iterações de uma fonna

entrelaçada.

15

O gss: a cada "thread" é atribuído um número decrescente de iterações de acordo

com a fórmula:

numero_iteracoes = iterações_resto/(2*numero_threads)+ 1

• chunksize: define o número de iterações por "threads" quando o schedtype é especificado por "dynamic" ou "interleave".

Convém ressaltar que loops pfor não podem ser aninhados.

5.2.6 - Execução paralela independente

Blocos de código independentes são definidos dentro de uma região paralela por:

tpragma independent

O bloco independente é executado pelo primeiro "thread" que o atinge, e é saltado pelos demais "threads ... Terminada a execução do bloco independente o "thread" continua executando o código restante da região paralela.

Exemplo:

tpragma independent {

bloco independente

5.2. 7 - Execução por apenas um processador

Blocos de códigos que devem ser executados por apenas um processador são definidos dentro de regiões paralelas por:

Exemplo:

tpragma one processar

tpragma one processar {

código executado por apenas um processador

5.2.8 - Seção crítica

Blocos que devem ser executados por um processador de cada vez, são defirudos por:

tpragma critica!

16

Exemplo:

tpragma critica! {

seçAo critica

5.3 - Linguagens de programação paralela

A disponibilidade de uma linguagem de programação paralela permite ao usuário explorar mais facilmente os recursos de paralelismo da máquina. Através de construções específicas, o usuário elabora programas legíveis, mais confiáveis, sem ter a preocupação de tratar com mecanismos de controle, como por exemplo, sincronizar o fluxo do programa, criar e retirar processos, através das chamadas das respectivas rotinas. Será apresentada aqui, ilustrando a programação através de uma linguagem de programação paralela, a linguagem CPAR [9], desenvolvida no Laboratório de Sistemas Integráveis. Esta linguagem foi projetada para fornecer recursos para a programação em sistemas multiprocessadores com memória compartilhada. É uma linguagem que em sistemas multiprocessadores com memória compartilhada e "clusters" de processadores se mostra extremamente adequada, pois permite explorar a hierarquia da memória compartilhada, presente nestes sistemas.

5.3.1 - A linguagem CPAR

No modelo de programação oferecido pela CPAR existem duas classes de paralelismo: "macrotasking" e "microtasking".

"Macrotask:ing" é a partição do programa em porções, denominadas macrotarefas, que podem ser executadas paralela e assincronamente. A macrotarefa deve envolver uma quantidade substancial de processamento, caracterizando uma granulação grossa.

"Microtask:ing" é um nível mais fino de paralelismo, envolvendo paralelismo homogêneo, presente em "loops", e paralelismo heterogêneo, onde blocos distintos de instruções são executados paralelamente. O processamento em "microtask:ing" é efetuado por microtarefas.

Em cada macrotarefa podem estar presentes múltiplas microtarefas que são executadas paralelamente pelo~ processadores reservados para a macrotarefa.

Blocos de porções do programa podem ser executadas paralelamente, como mostra a figura 6.

17

~ COITUJdor: ctxJi&o Lf seqflencial

Figura 6 - Modelo de paralelismo.

Uma evolução da implementação da CP AR pennite vários níveis de blocos de porções do programa executados paralelamente, através de aninhamentos dos blocos. A figura 7 ilustra este modelo.

comandos)

comandos6

Figura 7 - Modelo com aninhamento de blocos.

A linguagem CPAR apresenta as seguintes características:

• possui primitivas para a declaração e execução de macrotarefas;

• possui primitivas para explicitar microtarefas;

• pennite a declaração de variáveis locais e de variáveis compartilhadas globais e locais à macrotarefa;

18

• fornece uma construç!o denominada monitor que implementa um mecanismo de exclus!o mútua;

• a comunicaç!o entre macrotarefas pode ser realizada por passagem de mensagem ou memória compartilhada;

• a sincronizaç!o entre macrotarefas é efetuada por rotinas da biblioteca de paralelismo;

• pennite o particionamento do programa em blocos de porções, envolvendo inclusive macrotarefas, como mostrado na figura 7;

• pennite a presença de "loops, e blocos de instruções paralelos em subrotinas especiais definidas como paralelas, e que podem ser chamadas apenas por macrotarefas. Variáveis compartilhadas declaradas dentro de tais subrotinas são alocadas na área de memória compartilhada local da macrotarefa solicitante.

O exemplo 3 mostra um programa que utiliza as construções oferecidas pela CPAR.

Exemplo 3 :

19

Exemplo 3 : (continuação)

O exemplo 4 ilustra o particionamento do programa em blocos, com a presença de macrotarefas dentro do bloco.

20

Exemplo 4 :

21

· 6. Paralelização Automática

Uma outra forma para explorar o paralelismo é escrever um programa seqüencial e deixar a cargo de um paralelizador automático a tarefa de gerar o programa paralelo. O paralelizador automático3

analisa os comandos do programa sequencial, detecta as principais fontes de paralelismo. e, através da reestruturação de código, gera uma versão paralela do programa (fig.8) .

. Paralelizador Automático

Figura 8 - Paralelizador automático.

Wolfe cita em seu trabalho [12] a existência de três níveis de paralelismo para um programa:

• paralelismo a nível de subrotina: diversas chamadas a uma rotina são executadas simultaneamente;

• paralelismo a nível de bloco básico: diversas operações de um único bloco básico são executados em paralelo; e

• paralelismo a nível de "loops": diversas iterações de um comando de laço são executados por diversos processadores.

Normalmente, o terceiro nível de paralelismo é o mais comum, sendo utilizado com maior frequência. Programas costumam passar a maior parte do tempo executando dentro de "loops". Desta forma, os paralelizadores automáticos procuram explorar este nível. O principal objetivo destes sistemas é utilizar todos os processadores existentes para a execução das iterações de um "loop".

6.1 - Dependência de Dados

A dificuldade para a paralelização de "loops" ocorre porque se está tentando executar, em paralelo, comandos projetados para execução seqüencial.

Vejamos o seguinte exemplo:

a = b + 1; (1) c = a + 7; (2)

Os comandos (1) e (2) não podem ser ser executados em paralelo, pois o segundo comando depende do valor atualizado de a para poder ser processado. Esta dependência de valores existente entre comandos é chamada "dependência de dados".

3. também chamado compilador paralelizante ou supercompiladot

22

Existem três tipos de dependência de dados que podem ser encontrados em programas escritos em

linguagens tradicionais:

• dependência de fluxo (ou dependência verdadeira);

• anti-dependência; e

• dependência de saída.

A dependência de fluxo é aquela onde um comando atualiza o valor de uma variável, que é depois

referenciada em outro comando. Por exemplo:

a = b + c; (1)

d = a - e; (2)

O comando (2) depende do comando (1), pois ele utiliza o valor da variável a que é calculada no primeiro comando. Deste modo, o comando (2) deve ser executado após o término da execução do comando ( 1 ).

A anti-dependência ocorre quando um comando lê uma variável que é atualizada por outro comando. Por exemplo,

a = b + 1; (1) b = c + d; (2)

O comando (2) atualiza o valor de b, que foi referenciada pelo comando (1). Note que o primeiro comando deve utilizar o valor antigo de b em seu cálculo. Ou seja, o comando ( 1) deve necessariamente ser executado antes do comando (2).

A dependência de safda ocorre quando dois comandos atualizam o valor de uma mesma variável. Por exemplo:

a = b + 1; (1) a = c - d; (2)

Os dois comandos recalculam o valor de a. Neste caso, o comando ( 1) deve ser executado primeiro, para garantir que o valor de a após a execução de (1) e (2) seja aquele alterado pelo segundo comando.

As dependências de dados restringem a execução paralela de um programa, pois elas definem uma ordenação na execução dos comandos. Ou seja, alguns comandos devem ser executados somente após o término de outro, restringindo assim a possibilidade de se executar estes comandos paralelamente.

A dependência de fluxo é chamada geralmente como dependência verdadeira, pois representa uma restrição real na ordem de execução dos comandos de um programa. Ou seja, a ordenação textual dos comandos deve ser mantida, caso contrário, a semântica original do programa é alterada.

As outras formas de dependência de dados, anti- dependência e dependência de saída, são chamadas falsas dependências, pois decorrem de práticas de programação usualmente adotadas. Estas relações de dependência podem ser facilmente removidas pela inserção de novas variáveis ao programa.

23

6.2- Grafo de Dependências

As relações de dependência de um programa são normalmente representadas sob a forma gráfica, o grafo de dependências. Esta forma de representação é muito interessante pois permite uma visão conveniente das dependências entre os comandos de um programa e o uso de algoritmos de grafos para sua análise, por exemplo, para encontrar um ciclo de dependências.

Um grafo de dependências G de um programa é composto por n nós, um para cada comando Si

(l~i~n). Cada relação de dependência entre dois comandos Si e Sj tem um arco correspondente em O do nó representando Si para o nó representando Sj-

0 seguinte trecho de código

Sl: a = b + d; S2: c = a * 3; S3: a = a + c; S4: e = a I 2;

tem o seguinte grafo de dependências correspondente (fig.9).

Figura 9 - Grafo de Dependências.

6.3 - Dependência em Loops

Quando um programa envolve o acesso a vetores ou matrizes, a análise das relações de dependências

deve levar em conta os índices ("subscripts") para determinar se os mesmos elementos estão sendo

referenciados. Considere o seguinte trecho de programa [5].

for (i=O ; i<N; i++) S1 : a[i+K) = •••

S2: . . . = a [i 1;

onde K é uma constante inteira não negativa. S 1 é executado antes de S2 numa execução sequencial

do programa. Uma dependência existe entre os dois comandos se existir dois valores I 1 e h do índice i para os quais

O ~ I 1 ~ I 2 ~ N e I 1 + K = I 2

24

A análise destas condições mostra que se K > N não existe dependência de dados e se K ~ N uma dependência pode existir entre S 1 e S2. Como normalmente N não é conhecido em tempo de compilação, o compilador paraJelizante pode assumir por "segurança" que N é n;tuito grande.

Para o seguinte trecho de código

for (i=O; i<N; i++) S1 : a = b(i] + 1; S2: c[i] = a + 2;

temos o correpondente grafo de dependência (fig.lO).

Figura 10- Grafo de dependências para "loops".

6.4 - Análise de Dependências

Um compilador paralelizante deve fazer uma análise das relações de dependência para todos os pares de comandos de um programa de forma a gerar o programa paralelo correpondente. Para executar tal análise são empregados diversos algoritmos de decisão de dependência de dados.

Analisamos aqui apenas dois algoritmos bem simples: o Teste do GCD e o Teste de Banerjee. Estes algoritmos são exemplos clássicos para a determinação de dependência de dados em loops.

Devemos ressaltar que normalmente os testes de dependência não são precisos e, no caso de dúvida na determinação da dependência, o compilador paralelizante assume a existência por segurança.

Para explicarmos este dois testes consideremos o seguinte trecho de c6digo4

for (i=L; i <U; i+=N) Si: X[f(i)] Sj: ... X[g(i)] .. .

Existe uma dependência entre Si e Sj se houver dois valores 11 e 12 tal que

Representando f(.) e g(.) como os polinônios

f(i) = Ao + A1 * i g( i ) = Bo + B1 * i

4. consideramos por simplicidade apenas o caso de vetores unidimensionais.

· temos que

ou

onde

c l = Al c 2 = -A2 D = Bo - Ao

6.4.1 - Teste do GCD

25

O teste do GCD5 é o teste mais simples para detectar a independência de dados entre dois comandos. Podemos dizer que há dependência de dados entre Si e Sj se D é divisível por gcd (Cb C2)

Vejamos um exemplo: o seguinte trecho de código

for (i=O; i<10 ; i++) { S1 : a[i + 2] = c+ 10;

S2 : d = a[2*i- 7];

tem os comandos s 1 e s2 dependentes de dados, pois temos

il - 2i2 = -9 e gcd (1,2) = 1

A equação diofantina tem solução para o par ordenado (ih i2) = (1, 5), por exemplo.

6.4.2 - Teste de Banerjee

O teste de Banerjee, também chamado inequações de Banerjee, pode ser enunciado da seguinte forma:

Sejam LB e UB os limites inferior e superior de A1 * 11 - A2 * l2, considerando L~ 11 ~ 12 ~ U.

Há dependência de dados se

LB ~ B0 - Ao ~ UB.

No exemplo acima temos

LB = -18 UB = 9

e B0 - Ao = - 9

5. gcd = mdc (máximo divisor comum).

26

O que satisfaz as inequações. Conclui-se então que existe dependência de dados entre S1 e S2.

6.5 - Transformações

De modo a eliminar algumas das dependências de dados existentes em um programa, aplica- se transformações aos comandos, possibilitando assim a sua paralelização. Apresentamos, a seguir,

uma rápida descrição de algumas transformações6.

6.5.1 - Reordenação de Loops:

Consideremos o seguinte trecho de programa:

f o r (j=1; j <N; j++) f o r (i=1; i <N; i++)

a[j) [i) = a[j-1) [i) + b [j) [i);

O "loop" mais externo não pode ser paralelizado porque há uma dependência entre instâncias. Neste caso, os dois "loops" podem ser trocados de modo que o "loop" mais externo não apresente nenhuma dependência. Deste modo, cada processador terá uma quantidade de trabalho maior, melhorando assim a eficiência do código.

6.5.2 - Movimentação de "If Invariante":

Quando a condição de um comando if é invariante no "loop", este comando pode ser colocado fora do "loop", aumentando o desempenho do programa. Consideremos o seguinte exemplo:

f o r (i=O; i<N; i ++) a [i) = b[i ) + c [i); (1) i f (X > 0)

a[i) * = x; ( 2 )

O comando (2) pode ou não ser executado, dependendo do valor de x. Deste modo, aplicando a transformação temos:

i f (X > 0)

f or (i=O; i <N; i++) a[i) = b[i] + c[i); a[i) *= x;

else f o r (i=O; i <N; i++)

a[i) = b[i) + c[i );

6. para maiores informações veja referências [5] [7] [14].

27

6.5.3 - Distribuição de ''Loop":

Seja o seguinte trecho de código:

for (i=1; i<N; i++) a [i+1] = b [i-1] + c [i); (1) b[i] = a[i] * k; (2) c[i) = b[i] - 1; (3)

Este "loop" não pode ser paralelizado, pois existe dependência de dados entre todos os comandos. Os comandos (1) e (2) dependem um do outro devido ao cálculo de a e b, enquanto que (3) depende de (1) e (2) (cálculo de b). Neste caso, podemos separar o comando (3) em um "loop" separado, e depois paralelizá-lo. Assim, o trecho transformado fica:

for (i=1; i<N; i++) a[i+1] = b[i-1] + c[i); (1) b[i) = a[i) * k; (2)

forall i=l to N-1 c[i] = b[i]- 1; (3)

6.5.4 - "Node Splitting":

Vejamos o seguinte "loop":

for (i=1; i <N; i++) a[i) = b[i) + c[i); (1) d [i] = a [i- 1] * a [i+l); (2)

Ele não pode ser paralelizado por que há uma dependência entre os comandos (1) e (2) devido ao acesso aos elementos do vetor a . O comando (2) depende de (1) devido ao cálculo de a [i -1) . Por outro lado, (1) depende de (2) por causa do cálculo de a [i) (a i- ésima iteração de (2) deve ser executado antes da (i+l)- ésima iteração de (1)).

Analisando este trecho de código, verificamos que os valores de a [ i+ 1 ) , usados em (2), já podem ser conhecidos de antemão. Assim, seu cálculo pode ser isolado (através do vetor auxiliar temp).

for (i=1; i <N; i++) temp [i] = a[i+1 ] ; a[i] = b[i] + c[i]; (1) d [i] = a [i-1] * temp [i]; (2')

28

E assim, este "loop" pode ser transformado no seguinte:

forall i-=1 to N-1 ternp [i] = a[i+1];

forall i=1 to N-1 a[i] "" b [i] + c [i] i (1)

forall i=1 to N-1 d [i] = a[i-1] * ternp [i]; (2 I )

6.6 - Estrutura de um Paralelizador Automático

Tipicamente, um paralelizador automático de programas pode ser visto como sendo composto de três fases (fig.ll):

+ "front end": executa as tarefas de análise de um compilador tradicional (análise léxica, sintática e semântica), cria uma representação intermediária do programa e efetua uma análise da estrutura do programa (análise de fluxo e de dependência de dados);

+ paralelização: consiste na aplicação de diversas transformações ao programa de forma a eliminar as dependências de dados existentes; e

+ "back end": executa a geração de código paralelo.

Diversos centros de pesquisa estão trabalhando em compiladores com múltiplos "front ends" e múltiplos "back ends", de modo que a fase de paralelização possa ser compartilhada.

De uma maneira geral, a maioria dos sistemas automáticos se baseiam na linguagem Fortran, mas recentemente surgiram sistemas para outras linguagens, como o Parafrase- 2 que aceita programas escritos em linguagem C.

Programa SeqOencial

Programa Paralelo

Figura 11 - Estrutura de um Paralelizador Automático.

6.7- Exemplos de Sistemas

Passamos agora a apresentar dois sistemas para paralelização automática de programas existentes.

29

6.7 .1 - Parafrase2

P arafrase-2 é um projeto de pesquisa em desenvolvimento no Center for Supercomputing Research and Development da University of lllinois at Urbana- Champaign [6]. Ele é um dos primeiros projetos de um paralelizador automático com suporte a diversas linguagens de programação 7. A estrutura do sistema é representado na figura 12.

Figura 12- Estrutura do Parafrase-2.

Entre suas principais características, podemos citar:

+ Multilinguagem: a representação intermediária permite que qualquer linguagem procedural seja aceita;

• Interface Gráfica: utiliza-se do X-Window para mostrar o grafo de dependência, permitindo modificá- lo manualmente. Apresenta também o grafo de fluxo, o grafo de tarefas e o grafo de chamadas;

• Portabilidade: Parafrase-2 é codificado em C e, portanto, facilmente portável em diversas plataformas;

+ Análise de dependência: analisa dependência de dados e de controle;

• Análise interprocedural: efetua análise de dependência na presença de funções e procedimentos;

• Grafo hierárquico de tarefas: permite a exploração de paralelismo não estruturado ou funcional.

Uma linha de chamada para a versão C do Parafrase-2 tem o seguinte aspecto:

7. Atualmente, Parafrase-2 reconhece Fortran e C.

30

p2cpp -p passos.C teste . c

onde, a opção -p indica o arquivo de passos, passos . C. O arquivo de passos indica a seqUência de passos a serem seguidos pelo Parafrase-2 durante o processamento do arquivo de entrada. Cada passo deve estar numa linha separada. Por exemplo, um arquivo de passos pode ter o seguinte aspecto:

• arquivo de passos exemplo fixup • passo necessário callgraph • constrói grafo de chamada buildep t constrói grafo de dependência loop_interchange t executa transformaçAo de troca de "loops" codegen saida.c t gera código paralelo em saida.c

A descrição completa dos passos disponíveis se encontra em [8].

6.7.2- lris Power C Analyser

/RIS Power C Analyser (PCA) faz parte de um sistema de paralelização automático de programas disponível para os sistemas multiprocessadores IRIS da Silicon Graphics. O PCA é um pré-processador de código C que detecta fontes de paralelismo em programas sequenciais e acrescenta diretivas paralelas. Ele pode ser utilizado separadamente ou como parte integrante do compilador C. A figura 13 ilustra o processo de compilação do sistema Power C.

Figura 13 - Compilação com o Power C.

31

O PCA pode ser ativado pela linha de comando do compilador C através da opção -pca. Deste modo,

para compilar um programa, a linha de comando:

cc -pca list teste.c

produzirá um arquivo executável a.out, e um arquivo de listagem da análise do PCA teste./.

Por exemplo, suponhemos que o arquivo teste.c se pareça com o seguinte:

O arquivo de listagem teste./ será similar a este:

Analisando a listagem acima vemos que PCA paralelizou o primeiro "loop" (que inicia o vetor a) e manteve a execução sequencial do segundo devido à dependência de dados sobre a variável sorna.

O PCA possui outras opções, por exemplo, para manter o programa C paralelizado, para executar análise interprocedural, "inlining", etc. Para maiores informações consulte referência [11].

Como uma ferramenta de análise separada, o PCA pode ser ativado com a seguinte linha de comando

32

/usr/lib/pca [opções) .. . arquivo.c

PCA.deve ser executado após o pré-processador C (cpp). Por exemplo, para analisar o arquivo de teste, pca deve ser chamado da seguinte forma

/usr/lib/pca -i=teste . c -l=teste.l -cmp=teste.rn -lo=ls

A opção -crnp diz ao PCA guardar o programa paralelizado no arquivo nomeado. Para o programa teste, o programa otimizado se parace com o seguinte

O segundo "loop" não foi paralelizado pois se trata de urna redução de dados. Caso erros de arredondamento não sejam críticos, o segundo "loop" pcxle ser paralelizado de forma segura. A opção -roundoft (- r) pcxle ser utilizada para especificar isto.

/usr/lib/pca -i=teste.c - l =teste.l - cmp=teste.rn -lo=ls -r=2

A opção -r=2 faz com que o PCA marque as reduções para serem executados em paralelo.

Após a execução, ambos os loops são paralelizados e o arquivo de listagem é similar ao seguinte

33

O arquivo intermediário, teste.m, se parece com o seguinte.

6.8 - Como Paralelizar seu Programa

De uma maneria geral, os dois métodos para se explorar o paralelismo (manual e automático) têm suas vantagens e desvantagens. Desta forma, o melhor método para paralelizar um programa é aproveitar as vantagens dos dois métodos, utilizando-os iterativamente, conforme ilustrado na figura 14.

34

Paralelizaçlo Autom6tica

Modificaçlo/recodificaçlo de porções do código

nao ok fonte e/ou modificaçlo dos parimetros

do paralelizador

Figura 14- Usando os métodos manual e automático iterativamente.

7. Algoritmos Paralelos

Passamos agora a ver uma aplicação de um programa paralelo. A multiplicação de matrizes é um dos exemplos mais simples. Usaremos este exemplo para ilustrar como um algoritmo simples pode ser reestruturado para atender aos requisitos de arquitetura no qual vai ser executado. Matematicamente, os elementos C1 , j da matriz produto são relacionados com os elementos Ai, j

e Bi, j das matrizes sendo multiplicadas, pela seguinte equação: n

cij = I Ai.t x B~cj k=O

para 1 S i,j S n.

São três os métodos existentes para a implementação de uma rotina para multiplicação de matrizes.

+ inner- product

+ middle-product

+ outer- product

Passamos a fazer uma breve descrição de cada um destes métodos. Para maiores informações veja a referência [3] .

7.1- Inner-product

Em computadores seqüenciais, as matrizes são multiplicadas usando três loops aninhados, que são a tradução direta da definição acima.

for (i=O; i<n; i++) for (j=O; j <n; j++)

for (k=O; k<n; k++) S : c[i) [ j ) = c[i) [j) + a[i) [k) * b[k) [j];

35

Assume-se que os elementos da matriz c é iniciada com zeros. O comandoS dentro do "loop" mais interno forma o produto interno da i-ésirna linha de a e a j-ésirna coluna de b. Desta forma, o cálculo dos elementos de c envolve n 2 produtos internos. A fig. l5 mostra como é este processo de multiplicação.

X

c c a b

Figura 15 - lnner-product.

7.2 - Middle-product

Este método permite o cálculo de n produtos internos simultaneamente. Ele pode ser implementado trocando a ordem dos loops da seguinte forma

for (i=O; i<n ; i++) for (k=O; k <n; k++)

;-- -- - ---- ----------------------- - -- ·- ---- -- - ---for ( j=O; j<n; j++)

S: c[i) [j) = c[i) [j) + a[i) [k ) * b[k) [j);

- - - - .. - - - - - - - - - - - - - - - - .. - - - - - - - - - - - - - - - - - - - - - - - - ·'

Assim, todos os elementos sobre j podem ser calculados em paralelo (fig.16).

c c a

Figura 16 - Middle-product.

b

Uma forma para visualizar isto é substituir o "loop" mais interno por uma instrução vetorial correspondente

for (i=O ; i<n; i++) for (k=O; k<n; k++)

c[i) [l:n) = c[i) [l:n) + a[i] [k) * b[k) [l:n);

ou sob a forma paralela

for (i=O; i<n; i++) for (k= O; k<n; k++)

forall j=O to n-1 { c[i) [j) = c[i) [j) + a[i] [k) * b[k) [j);

36

Esta instrução tem a forma "vetor+ escalar x vetor". Isto faz com que este método seja adequado para máquina vetoriais como o Cray-1 . Neste computador, é verificado um desempenho de 138 MFLOPS, o que significa quase duas operações aritméticas por ciclo de relógio (uma operação por ciclo no Cray-1 equivale a 80 MFLOPS).

7.3- Outer-product

Este método permite a realização de n 2 produtos internos em paralelo. Isto faz com que ele seja adequado em maquinas que tenham as mesmas dimensões das matrizes e apresentem uma arquitetura matricial.

Este método é obtido movendo o "loop" em k para fora

for (k=O; k<n; k++) f----- --- .... --.---- .... -- .. - .. - .. ----- .. - .. -- ...... ------ .... - .... - .. -- ·

for (i=O; i<n; i++) for (j=O; j<n; j++)

S: c[i] [j] = c[i] [j] + a[i] [k] * b[k] [j);

· -- .. -- - -- - .. ----- -- .. ----- .. - - .. .. --- .... --------- .. - .. -- .. .... ... -- J

Temos então os n 2 elementos de c sendo calculados simultaneamente (fig.17).

+

c c a b

Figura 17 - Outer- product.

O "loop" mais externo pode ser paralelizado e a multiplicação pode ser feita do seguinte modo:

forall k=O to n-1 { for (i=O; i<n; i++)

for ( j=O; j<n; j++) c[i] [j] = c[i] [j] + a[i] [k] * b[k] [j];

37

· 8. Conclusão

Na busca do máximo aproveitamento dos múltiplos processadores disporuveis, várias extensões de linguagens, como aquelas para o Fortran, C e LISP, têm sido propostas. Novos modelos e linguagens de programação paralela deverão surgir, visando uma especificação mais adequada dos problemas e a utilização eficiente dos recursos disporuveis das máquinas.

Compiladores paralelizantes que aplicam transformações no programa, minimizando as

dependências de dados presentes, e implementam o paralelismo principalmente em "loops" estão

disporuveis no mercado. A pesquisa nesta área continua em sua busca de maior aproveitamento de paralelismo em "loops" e se volta agora para a exploração fora delas também.

A tendência da pesquisa é convergir estas linhas, ou seja, futuramente os ambientes de programação paralela permitirão ao programador utilizar linguagens paralelas na implementação de programas e, através de ferramentas, para1elizar automaticamente trechos paralelizáveis não explícitos.

Agradecimentos

Os dois primeiros autores gostariam de agradecer ao Prof. Constantine D. Polychronopoulos pela oportunidade em trabalhar no Center for Supercomputing Research and Development da University of Illinois at Urbana- Champaign, onde puderam estudar e utilizar o sistema Parafrase- 2.

Bibliografia

[1] BAUER, B. E. Practical parallel programming. San Diego, Academic Press, 1992.

[2] CALLAHAN, D.; KENNEDY, K. Compiling programs for distributed memory multiprocessors. The Journal of Supercomputing, v.2, n.2, p.151- 69, 1988.

[3] HOCKNEY, R. W.; JESSHOPE, C. R. Parallel Computing 2. Bristol, Adam Hilger, 1988.

[4] OUSTERHAUG, A. (ed.) Guide to parallel programming on Sequent computer systems. 2.ed.

Englewood Cliffs, Prentice-Hall, 1989.

[5] POLYCHRONOPOULOS, C. D. Parallel programming and compilers. Boston, K.luwer Academic

Publishers, 1988.

[6] POLYCHRONOPOULOS, C. D. et al. Tbe structure of Parafrase- 2: an advanced parallelizing

compiler for C and Fortran. In: GELERNTER, D. et al., eds. Languages and compilers for

parallel computing. Cambridge, MIT Press, 1990. p.423- 53.

[7] POLYCHRONOPOULOS, C. D.; BECKMAN, C. J. Issues on compilers and operating systems for

highly parallel computers. In: JORNADA EPUSP/lEEE EM SISTEMAS DE

COMPUTAÇÃO DE ALTO DESEMPENHO, São Paulo, SP, 1991. Anais. LSI- EPUSP, São Paulo, 1991. (v. Cursos Internacionais)

[8] POLYCHRONOPOULOS, C. D. et ai. Parafrase-2 User 's Manual. 1991.

[9] SATO, L. M. CPAR: programação paralela em multi processadores. In: JORNADA EPUSP{IEEE EM

SISTEMAS DE COMPUTAÇÃO DE ALTO DESEMPENHO, São Paulo, SP, 1991. Anais.

LSI- EPUSP, São Paulo, 1991. p.71-91.

38

· [10] SATO. L. M.; MIDORIKAWA, E. T. Aspectos de Programação Paralela In: JORNADA

EPUSP/IEEE EM SISTEMAS DE COMPUTAÇÃO DE ALTO DESEMPENHO, 2, São Paulo,

SP, 1992. Anais. LSI-EPUSP. São Paulo, 1992. v.2, p.13-35.

[11] SILICON GRAPHICS. IRIS Power C User's Guide. Mountain \1ew, 1990. (Document Number

007-0702-010)

[12] WOLFE, M. Multiprocessor synchroruzation for concurrent loops. IEEE Software, Los Alamitos,

v.5, n.1, p.34-42, Jan. 1988.

[13] WOLFE, M. Optimizing supercompilers for supercomputers. Cambridge, MIT Press, 1989.

[14] ZIMA, H.; CHAPMAN, B. Supercompilers for parallel and vector computers. Wokingham,

Addison-Wesley, 1991.