CES-41
COMPILADORES
Capítulo Extra
Compiladores Paralelos
Capítulo Extra
Compiladores Paralelos
E.1 – Linguagens para processamento paralelo
E.2 – Componentes de um compilador paralelo
E.3 – Análise de dependências de dados
E.4 – O processo de paralelização de programas
E.1 – Linguagens para
processamento paralelo
E.1.1 - Conceito de processamento paralelo

Supercomputadores: computadores com milhares de
processadores e milhares de módulos de memória

Multiprocessamento: execução simultânea de vários
programas

Processamento paralelo: execução simultânea de vários
trechos de um mesmo programa
E.1.2 - Alternativas para linguagens

As linguagens utilizadas em processamento paralelo se
classificam em:
–
–
–

Linguagens paralelas
Linguagens convencionais ampliadas
Linguagens sequenciais
Linguagens paralelas e convencionais ampliadas:
–
–
–
Exigem recodificação de todos os programas sequenciais
de interesse, escritos até hoje
Há mais de 50 anos de programação sequencial
Obrigam muitos cientistas e pesquisadores a aprendê-las

Linguagens sequenciais: alternativa muito pesquisada na
programação de computadores paralelos:
–

Desobrigam do aprendizado de novas linguagens (inércia
dos cientistas e pesquisadores)
A utilização de linguagens sequenciais em máquinas paralelas
exigem do compilador a detecção automática de
paralelismo dos programas

Outras utilidades da detecção automática de paralelismo:
–
Em linguagens paralelas, para descobrir paralelismo não
explícito pelo programador
–
Quanto menor o nível de paralelismo, mais difícil
expressá-lo (iterações paralelas de laços, comandos
paralelos internos às iterações, paralelismo intracomandos)
–
Para adequar o paralelismo dos programas ao potencial
de paralelismo da máquina
E.1.3 - Paralelismo em programas sequenciais

Programas sequenciais apresentam muito paralelismo
implícito
–

Uma das maiores fontes de paralelismo é o laço
sequencial
Exemplos:
1)
C1:
C2:
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
D[i]  E[i] + F[i];
}
Todas as instâncias de C1 e de C2 podem ser executadas
simultaneamente
2)
C1:
C2:
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
D[i]  A[i] + E[i];
}
C1[1]
C1[2]
C1[3]
C1[4]
C1[5]
C2[1]
C2[2]
C2[3]
C2[4]
C2[5]
Todas as iterações podem ser executadas simultaneamente

Alguns laços não apresentam paralelismo

Exemplo:
C1:
C2:
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
B[i+1]  A[i] + D[i];
}
C1[1]
C1[2]
C1[3]
C1[4]
C1[5]
C2[1]
C2[2]
C2[3]
C2[4]
C2[5]
Há dependências intra e inter iterações
As iterações e os seus comandos internos são sequenciais

Há comandos a princípio sequenciais, mas que podem ser
transformados de forma a apresentar paralelismo

Exemplo: seja o seguinte laço:
C1:
C2:
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
D[i]  A[i] + A[i+1];
}
C1[1]
C1[2]
C1[3]
C1[4]
C1[5]
C2[1]
C2[2]
C2[3]
C2[4]
C2[5]
Há uma dependência de fluxo de C1[i] para C2[i] e uma
antidependência de C2[i] para C1[i+1]

Há comandos a princípio sequenciais, mas que podem ser
transformados de forma a apresentar paralelismo

Exemplo: seja o seguinte laço:
C1:
C2:
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
D[i]  A[i] + A[i+1];
}
C1[1]
C1[2]
C1[3]
C1[4]
C1[5]
C2[1]
C2[2]
C2[3]
C2[4]
C2[5]
Novamente as iterações e os comandos delas internos são
sequenciais
C1:
C2:
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
D[i]  A[i] + A[i+1];
}
No entanto, pode-se alterar o escopo do laço de forma a
introduzir nele paralelismo:
C2’:
C1:
C2:
for (i  1; i  5; i++) {
T[i]  A[i+1];
A[i]  B[i] + C[i];
D[i]  A[i] + T[i];
}
O laço pode ser decomposto em dois:
C2’:
C1:
C2:

for (i  1; i  5; i++)
T[i]  A[i+1];
for (i  1; i  5; i++) {
A[i]  B[i] + C[i];
D[i]  A[i] + T[i];
}
Em cada laço, todas as
iterações podem ser
executadas
simultaneamente
Assim, um compilador de uma linguagem sequencial para
processamento paralelo deve fazer:
–
Análise de dependências do programa fonte
–
Transformações no programa fonte, introduzindo-lhe
mais paralelismo
E.2 – Componentes de um
compilador paralelo

Grosso modo, um compilador paralelo de linguagens
sequenciais deve ter os seguintes componentes:
1. Analisador sintático e semântico, armazenando o
programa fonte numa estrutura adequada
2. Construtor do grafo de dependências do programa, fonte
de consultas para as transformações subsequentes
3. Eliminador de dependências e redutor de seus ciclos,
visando aumentar o potencial de paralelismo do programa
fonte
E.2 – Componentes de um
compilador paralelo

Grosso modo, um compilador paralelo de linguagens
sequenciais deve ter os seguintes componentes:
4. Reorganizador de laços, tornando-os tratáveis pelo passo 6
5. Alocador de memória, visando boa utilização de memória
local e global
6. Paralelizador propriamente dito, transformando os
comandos e laços sequenciais em equivalentes paralelos
7. Gerador de código intermediário e objeto
E.3 – Análise de Dependências de
Dados
E.3.1 – Tipos de dependências
a) Dependência de fluxo:

Um comando produz um valor e outro o utiliza

Exemplo:
C1:
C2:
A  B + C;
D  A + E;
Simbologia: a dependência é expressa por (C1 f C2)
b) Antidependência:

Um comando usa o valor de uma variável e outro o altera

Exemplo:
C1:
C2:
A  B + C;
B  D + E;
Simbologia: a dependência é expressa por (C1 a C2)
c) Dependência de saída:

Um comando produz um valor para uma variável e depois
outro lhe produz outro valor

Exemplo:
C1:
C2:
A  B + C;

A  D + E;
Simbologia: a dependência é expressa por (C1 s C2)
d) Dependência condicional:

O resultado da condição de um comando determina se
outro comando será ou não executado

Exemplo:
C1:
C2:
if (A < B)
C  D + E;
Simbologia: a dependência é expressa por (C1 c C2)
E.3.2 – Grafos de dependências

O conceito de dependência pode ser estendido para o
escopo de laços

Exemplo: um laço e a lista de suas dependências:
for (i  1; i  n; i++) {
C1:
a  X[i] + X[i-1];
C2:
Z[i]  a ** 2;
C3:
X[i]  X[i-1] + B[i];
}

Dependências:
(C1
(C1
(C1
(C2
(C3
(C3
s
f
a
a
f
f
C1)
C2)
C3)
C1)
C1)
C3)
Concebe-se o grafo de dependências: nível de comandos,
nível de instância e nível atômico
for (i  1; i  n; i++) {
C1:
a  X[i] + X[i-1];
C2:
Z[i]  a ** 2;
C3:
X[i]  X[i-1] + B[i];
}

Dependências:
(C1
(C1
(C1
(C2
(C3
(C3
s
f
a
a
f
f
C1)
C2)
C3)
C1)
C1)
C3)
Nível de comandos:
Todas as dependências
estão num só ciclo;
ciclos têm difícil solução
paralela
for (i  1; i  n; i++) {
C1:
a  X[i] + X[i-1];
C2:
Z[i]  a ** 2;
C3:
X[i]  X[i-1] + B[i];
}

Nível de instâncias:
Dependências:
(C1
(C1
(C1
(C2
(C3
(C3
s
f
a
a
f
f
C1)
C2)
C3)
C1)
C1)
C3)
for (i  1; i  n; i++) {
C1:
a  X[i] + X[i-1];
C2:
Z[i]  a ** 2;
C3:
X[i]  X[i-1] + B[i];
}

Nível atômico:
Mostra os elementos de
variáveis responsáveis
pelas dependências
Dependências:
(C1
(C1
(C1
(C2
(C3
(C3
s
f
a
a
f
f
C1)
C2)
C3)
C1)
C1)
C3)
E.3.3 – Definições para dependências

Existe uma dependência de dados (excluída a dependência
condicional) de um comando C1 para outro não
necessariamente distinto C2, simbolicamente (C1  C2), se e
somente se existirem instâncias C1’ e C2’ de C1 e C2 e uma
posição M de memória tais que:
1. C1’ e C2’ fazem referência a M e pelo menos uma dessas
referências é uma escrita
2. C1’ é executada antes de C2’
3. M não recebe escrita entre C1’ e C2’
1.
C1’ e C2’ fazem referência a M e pelo menos uma dessas
referências é uma escrita
2. C1’ é executada antes de C2’
3. M não recebe escrita entre C1’ e C2’





C1: fonte da dependência;
C1’: instância fonte
C2: destino da dependência; C2’: instância destino
Dependência de fluxo: C1’ é escrita e C2’ é leitura em M
Antidependência: C1’ é escrita e C2’ é leitura em M
Dependência de saída: C1’ e C2’ são escritas em M
Obs.: algumas referências bibliográficas incluem
o item 3 apenas para as dependências de fluxo
1.
C1’ e C2’ fazem referência a M e pelo menos uma dessas
referências é uma escrita
2. C1’ é executada antes de C2’
3. M não recebe escrita entre C1’ e C2’

Item 1: C1’ e C2’ fazem referência a M:
–
Para variáveis escalares é trivial: basta referenciarem a
mesma variável
–
Para variáveis indexadas não basta: é necessária uma
análise de seus subscritos
1.
C1’ e C2’ fazem referência a M e pelo menos uma dessas
referências é uma escrita
2. C1’ é executada antes de C2’
3. M não recebe escrita entre C1’ e C2’

Item 2: C1’ é executada antes de C2’; seja o seguinte laço:
C2:
C1:
for (j  1; j  n; j++) {
for (i  1; i  n; i++) {
D[i]  A ** 2;
A  E[i] + F[i];
}
A  B[j] + C[j];
}
A dependência
(C1 f C2) existe
Se o laço j não existisse
a dependência não
existiria
1.
C1’ e C2’ fazem referência a M e pelo menos uma dessas
referências é uma escrita
2. C1’ é executada antes de C2’
3. M não recebe escrita entre C1’ e C2’

Item 3: M não recebe escrita entre C1’ e C2’; seja o seguinte
laço:
C2:
C1:
C3:
}
for (i  1; i  n; i++) {
C[i]  A ** 2;
A  D[i] + E[i];
F[i]  A ** 3;
A  G[i] + H[i];
A dependência (C1 f C2)
não existe
Se o comando C3 fosse
condicional, a dependência
existiria

Dentro de laços, dependências têm direção e distância

Exemplos: sejam os seguintes laços:
1)
for (i  1; i  n; i++) {
A[i]  B[i] + 5;
C[i]  A[i] * 2;
}
A dependência (C1 f C2)
é interna à iteração i
for (i  1; i  n; i++) {
A[i]  B[i] + 5;
C[i]  A[i-3] * 2;
}
A dependência (C1 f C2)
vai da iteração i até a i+3
C1:
C2:
2)
C1:
C2:
Direção:
Distância:
Direção:
Distância:
‘=’
zero
‘<’
3

Dentro de laços, dependências têm direção e distância

Exemplos: sejam os seguintes laços:
3)
C1:
C2:
for (i  1; i  n; i++)
for (j  1; j  n; j++) {
A[i, j]  B[i, j] + C[i, j] ;
D[i, j]  A[i-2, j+1] + E[i, j];
}
A dependência (C1 f C2) vai da
iteração (i, j) até a (i+2, j-1)
Vetor-direção:
Vetor-distância:
(‘<’, ‘>’)
(2, -1)
E.3.4 – Detecção de dependências

A detecção de dependências entre variáveis escalares pode
utilizar análise de fluxo de dados aplicada em otimização
de código, na construção de compiladores

Entre variáveis indexadas, além disso, é necessária uma
análise dos subscritos dessas variáveis

Seja o aninhamento a seguir
for (I1  P1; I1  Q1; I1 ++) {
Laços de C1:
for (I2  P2; I2  Q2; I2 ++) {

I1, I2, ... , Id, Id+1, ... , Ie
for (Id  Pd; Id  Qd; Id ++) {
for (Id+1  Pd+1; Id+1  Qd+1; Id+1 ++) {
Laços de C2:

I1, I2, ... , Id, Ie+1, ... , In
for (Ie  Pe; Ie  Qe; Ie ++) {
C1:
. . . V[expr1,1, expr1,2, … , expr1,m,] . . .
A dependência (C1  C2)
}
existe se e somente se

existirem instâncias
}
for (Ie+1  Pe+1; Ie+1  Qe+1; Ie+1 ++) {
C1(i1, i2, ... , id, id+1, ... , ie) e

C2(j1, j2, ... , jd, je+1, ... , jn)
for (In  Pn; In  Qn; In ++) {
C2:
. . . V[expr2,1, expr2,2, … , expr2,m,] . . .
tais que:
}
1. k | (1  k  m), expr1,k = expr2,k

}
2. k | (1  k  n), Pk  ik, jk  Qk
}

3. A instância de C1 é executada antes da instância de C2
}
}
1.
k | (1  k  m), expr1,k = expr2,k
2.
k | (1  k  n), Pk  ik, jk  Qk
3.
A instância de C1 é executada antes da instância de C2
Item 1: sistema de m equações com as incógnitas:
i1, i2, ... , id, id+1, ... , ie, j1, j2, ... , jd, je+1, ... , jn
Item 2 e 3: inequações com as mesmas incógnitas

Se esse sistema de equações e inequações tiver solução, a
dependência existe; caso contrário, não existe

Quando esse sistema é linear a literatura apresenta vários
métodos de solução
E.4 – O Processo de Paralelização
E.4.1 – Um programa tipicamente sequencial

Seja o seguinte programa sequencial a ser paralelizado e seu
grafo de dependências:
for (i  1; i  100; i++)
for (j  1; j  100; j++) {
C1:
a  X[i, j] + X[i, j-1];
C2:
Z[i, j]  a ** 2;
C3:
X[i, j]  X[i, j-1] + B[j];
}
Todos os comandos e dependências
estão num só ciclo do grafo
Não é possível nenhuma paralelização
E.4.2 – Expansão de variáveis escalares
for (i  1; i  100; i++)
C1:
C2:
C3:

for (j  1; j  100; j++) {
a  X[i, j] + X[i, j-1];
Z[i, j]  a ** 2;
X[i, j]  X[i, j-1] + B[j];
}
A variável a é a causa das seguintes dependências: (C1 s
C1), (C1 f C2) e (C2 a C1)

Seu local é utilizado por todas a iterações

Substituindo-a dentro do aninhamento pela variável
indexada AA, cada iteração referencia um local diferente
for (i  1; i  100; i++)
C1:
C2:
C3:
for (j  1; j  100; j++) {
a  X[i, j] + X[i, j-1];
Z[i, j]  a ** 2;
X[i, j]  X[i, j-1] + B[j];
}
C1:
C2:
C3:
for (i  1; i  100; i++)
for (j  1; j  100; j++) {
AA[i,j]  X[i, j] + X[i, j-1];
Z[i, j]  AA[i,j] ** 2;
X[i, j]  X[i, j-1] + B[j];
}
a  AA[100, 100];
C4:

O comando C2 ficou fora do ciclo
E.4.3 – Reordenação de comandos de atribuição

Os comandos C2 e C3 podem trocar de posição:
for (i  1; i  100; i++)
C1:
C3:
C2:

for (j  1; j  100; j++) {
AA[i,j]  X[i, j] + X[i, j-1];
X[i, j]  X[i, j-1] + B[j];
Z[i, j]  AA[i,j] ** 2;
}
O comando C2 pode ser vetorizado
O comando C4 foi
omitido, mas será
reconsiderado no final
E.4.4 – Vetorização de laços

É a geração de comandos vetoriais
for (i  1; i  100; i++) {
C1:
C3:
C2:
}
for (j  1; j  100; j++) {
AA[i,j]  X[i, j] + X[i, j-1];
X[i, j]  X[i, j-1] + B[j];
}
Z[i, 1: 100]  AA[i, 1: 100] ** 2

Os elementos Z[i, 1], Z[i, 2], ... , Z[i, 100] podem ser
calculados simultaneamente

O ciclo (C1 C3) pode ser quebrado
E.4.5 – Fissão de comandos

Um ciclo contendo antidependências pode ser quebrado
com a introdução de variáveis temporárias
for (i  1; i  100; i++) {
for (j  1; j  100; j++) {
C1:
C3:
C2:
}
AA[i, j]  X[i, j] + X[i, j-1];
X[i, j]  X[i, j-1] + B[j];
}
Z[i, 1: 100]  AA[i, 1: 100] ** 2
E.4.5 – Fissão de comandos

Um ciclo contendo antidependências pode ser quebrado
com a introdução de variáveis temporárias
for (i  1; i  100; i++) {
C11:
C12:
C3:
C2:
}
for (j  1; j  100; j++) {
T[i, j]  X[i, j];
AA[i, j]  T[i, j] + X[i, j-1];
X[i, j]  X[i, j-1] + B[j];
}
Z[i, 1: 100]  AA[i, 1: 100] ** 2
E.4.5 – Fissão de comandos

E, para a vetorização de C11 e C12, é necessária nova
reordenação:
for (i  1; i  100; i++) {
C11:
C12:
C3:
C2:
}
for (j  1; j  100; j++) {
T[i, j]  X[i, j];
AA[i, j]  T[i, j] + X[i, j-1];
X[i, j]  X[i, j-1] + B[j];
}
Z[i, 1: 100]  AA[i, 1: 100] ** 2
E.4.5 – Fissão de comandos

E, para a vetorização de C11 e C12, é necessária nova
reordenação:
for (i  1; i  100; i++) {
C11:
C3:
C12:
C2:
}
for (j  1; j  100; j++) {
T[i, j]  X[i, j];
X[i, j]  X[i, j-1] + B[j];
AA[i, j]  T[i, j] + X[i, j-1];
}
Z[i, 1: 100]  AA[i, 1: 100] ** 2
E.4.5 – Fissão de comandos

E, para a vetorização de C11 e C12, é necessária nova
reordenação:
for (i  1; i  100; i++) {
C11:
C3:
C12:
C2:
}
T[i, 1: 100]  X[i, 1: 100];
for (j  1; j  100; j++)
X[i, j]  X[i, j-1] + B[j];
AA[i, 1: 100]  T[i, 1: 100] + X[i, 0: 99];
Z[i, 1: 100]  AA[i, 1: 100] ** 2
E.4.6 – Decomposição parcial de laços

Um laço contendo vários comandos em seu escopo pode ser
decomposto num conjunto de laços menores

Geralmente a paralelização de laços pequenos é mais simples
que a de laços grandes

Assim o laço do programa anterior pode ser decomposto
conforme o programa logo a seguir
E.4.6 – Decomposição parcial de laços
C11:
for (i  1; i  100; i++)
T[i, 1: 100]  X[i, 1: 100];
C3:
for (i  1; i  100; i++)
for (j  1; j  100; j++)
X[i, j]  X[i, j-1] + B[j];
C12:
C2:

for (i  1; i  100; i++) {
AA[i, 1: 100]  T[i, 1: 100] + X[i, 0: 99];
Z[i, 1: 100]  AA[i, 1: 100] ** 2;
}
Neste grafo de dependências, cada aninhamento tem seu
grafo particular
E.4.7 – Paralelização total das iterações dos laços

No programa anterior, os grafos do 1o e do 3o aninhamentos
não apresentam ciclos

No grafo do 2o aninhamento, a dependência é interna à
iteração do laço externo (=, <)

Todos esses laços podem ser transformados em comandos
DoAll, conforme o programa a seguir

Todas a iterações de um DoAll são executadas
simultaneamente
E.4.7 – Paralelização total das iterações dos laços
E.4.9 – Resultado da paralelização

Resumindo o que foi feito, o comando sequencial,
desprovido de paralelismo:
for (i  1; i  100; i++)
for (j  1; j  100; j++) {
C1:
a  X[i, j] + X[i, j-1];
C2:
Z[i, j]  a ** 2;
C3:
X[i, j]  X[i, j-1] + B[j];
}

Foi transformado no programa paralelo a seguir
DoAll i = 1, 100
T[i, 1: 100]  X[i, 1: 100];
EndDoAll
DoAll (i  1; i  100; i++)
for (j  1; j  100; j++)
X[i, j]  X[i, j-1] + B[j];
EndDoAll
DoAll i = 1, 100
AA[i, 1: 100]  T[i, 1: 100] + X[i, 0: 99];
Z[i, 1: 100]  AA[i, 1: 100] ** 2;
EndDoAll
a = AA[100, 100];
Suponha que este programa seja
executado numa máquina com 100
processadores e que cada um deles
seja um processador matricial de
100 processadores síncronos
Tal máquina tem na realidade o
equivalente a 10000 processadores
escalares
Suponha também, grosso modo,
que cada comando de atribuição
seja executado num tempo igual a
t unidades de tempo
DoAll i = 1, 100
T[i, 1: 100]  X[i, 1: 100];
EndDoAll
DoAll (i  1; i  100; i++)
for (j  1; j  100; j++)
X[i, j]  X[i, j-1] + B[j];
EndDoAll
DoAll i = 1, 100
AA[i, 1: 100]  T[i, 1: 100] + X[i, 0: 99];
Z[i, 1: 100]  AA[i, 1: 100] ** 2;
EndDoAll
a = AA[100, 100];
A seguinte tabela mostra o tempo
de execução do laço sequencial e
das várias tarefas do programa
paralelo
DoAll i = 1, 100
T[i, 1: 100]  X[i, 1: 100];
EndDoAll
DoAll (i  1; i  100; i++)
for (j  1; j  100; j++)
X[i, j]  X[i, j-1] + B[j];
EndDoAll
DoAll i = 1, 100
AA[i, 1: 100]  T[i, 1: 100] + X[i, 0: 99];
Z[i, 1: 100]  AA[i, 1: 100] ** 2;
EndDoAll
a = AA[100, 100];
A baixa eficiência se deve à tarefa
T2, que utiliza somente 100 dos
10000 processadores, deixando
ociosos os outros 9900.
Se o ganho de 300 for
imprescindível, não importa essa
baixa eficiência.
DoAll i = 1, 100
T[i, 1: 100]  X[i, 1: 100];
EndDoAll
DoAll (i  1; i  100; i++)
for (j  1; j  100; j++)
X[i, j]  X[i, j-1] + B[j];
EndDoAll
DoAll i = 1, 100
AA[i, 1: 100]  T[i, 1: 100] + X[i, 0: 99];
Z[i, 1: 100]  AA[i, 1: 100] ** 2;
EndDoAll
a = AA[100, 100];
Porém se essa eficiência de 3% for
inaceitável, pode-se usar uma
arquitetura em que os 100
processadores não sejam
matriciais
Assim a tabela dos tempos de
execução será a seguinte:
Download

CES-41 Teoria Cap Extra