universidade federal do paranÁ - inf.ufpr.br · vazio e finito v de vértices e um conjunto finito...

47
UNIVERSIDADE FEDERAL DO PARANÁ SETOR DE CIÊNCIAS EXATAS DEPARTAMENTO DE INFORMÁTICA HIPERGRAFOS DIRIGIDOS À COMPUTAÇÃO PARALELA CURITIBA 2008 1

Upload: vanbao

Post on 01-Dec-2018

214 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

UNIVERSIDADE FEDERAL DO PARANÁSETOR DE CIÊNCIAS EXATAS

DEPARTAMENTO DE INFORMÁTICA

HIPERGRAFOS DIRIGIDOS À COMPUTAÇÃO PARALELA

CURITIBA

2008

1

Page 2: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Bruno Andrade Pedrassani

Francisco Miguel Bubniak Ribas

Rúben Sekiji Negrão Doi

HIPERGRAFOS DIRIGIDOS À COMPUTAÇÃO PARALELA

Trabalho apresentado ao Departamento deInformática da Universidade Federal do

Paraná (UFPR), como parte dos requisitosnecessários ao cumprimento da disciplina de

Trabalho de Graduação em Algoritmos e Grafos II.

Orientador: Prof. Dr. André Luiz Pires Guedes

CURITIBA

2008

2

Page 3: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Bruno Andrade Pedrassani

Francisco Miguel Bubniak Ribas

Rúben Sekiji Negrão Doi

HIPERGRAFOS DIRIGIDOS À COMPUTAÇÃO PARALELA

Esta pesquisa foi julgada adequada e aprovada comoTrabalho de Graduação II do Curso de Bacharelado em

Ciência da Computação, da Universidade Federal doParaná.

Curitiba, XX de agosto de 2008

Banca Examinadora:

_____________________________________Prof. Dr. André Luiz Pires Guedes

Orientador

_____________________________________Prof. Dr. Roberto A. Hexsel

Departamento de Informática

_____________________________________Prof. Dr. Luis Allan Künzle

Departamento de Informática

3

Page 4: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Agradecimentos

Acreditamos realmente que nada, seja qual for o momento da vida, se constrói sem a ajuda e o apoio de outras pessoas. Tudo aquilo que é construído com retidão é frutuoso.

Por isso gostaríamos de agradecer profundamente:

- Nossas famílias, por todo o apoio e compreensão incondicionais, em todas as horas difíceis;

- Ao professor André, por sua pronta e irrestrita ajuda e orientação;

- Todo o Departamento de Informática e à UFPR, por toda a estrutura humana e física disponibilizada;

4

Page 5: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

“ 'Não há rosas sem espinhos' é um ditado melancólico. Digamos ao invés disso: ' Não há espinhos sem rosas'”

Chiara Lubich.

5

Page 6: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Resumo

Nosso trabalho envolve uma pesquisa profunda sobre estrutura de dados, especificamente sobre os Hipergrafos dirigidos, e toda sua utilização na computação paralela.

Com o advento das novas tecnologias e formas de computação, em especial a computação paralela, o conceito de paralelismo fica cada vez mais importante, porém o impacto dessa mudança paradigmática torna-se uma barreira.

Foi-nos então proposto alguns problemas relacionados à essa mudança de paradigma, tais como otimizar a execução de processos utilizando-se um conjunto finito de unidades de processamento.

Após um profundo estudo de documentos, muitos sugeridos por nosso orientador, prof. André Guedes, outros encontrados durante a pesquisa, pudemos, de forma satisfatória, compreender de forma mais precisa os problemas, e, principalmente, as formas de resolvê-los.

Desenvolvemos então um conjunto de algoritmos que, como será visto durante este trabalho, conseguem trabalhar sobre um hipergrafo (estrutura abstrata de dados utilizada para respresentar a execução dos processos e suas interdependências), afim de otimizar da melhor maneira possível a execução dos processos de forma paralela.

palavras-chave: Hipergrafos, computação paralela, algoritmos, otimização.

6

Page 7: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

SumárioAgradecimentos..........................................................................................................................4Resumo........................................................................................................................................6Sumário.......................................................................................................................................71.Introdução................................................................................................................................92. Conceitos fundamentais........................................................................................................10

2.1 Grafo e grafo direcionado...............................................................................................102.2 Hipergrafo e hipergrafo direcionado..............................................................................102.3 Ordenação Topológica....................................................................................................122.4 Filas................................................................................................................................132.5 Pipeline...........................................................................................................................132.6 Bolhas de Execução........................................................................................................13

3. Representação computacional ..............................................................................................133.1 Matriz de adjacências.....................................................................................................143.2 Lista de adjacências........................................................................................................143.3 Representando Hipergrafos............................................................................................15

4. Problema proposto................................................................................................................164.1 Descrição do problema...................................................................................................164.2 Restrições do problema..................................................................................................17

5. Implementação......................................................................................................................175.1 Estrutura de representação do hipergrafo.......................................................................175.2 Estruturas auxiliares.......................................................................................................185.3 Definição da estrutura do arquivo de leitura do Hipergrafo...........................................195.4 Algoritmos para Cálculo das Soluções...........................................................................20

5.4.1 Algoritmo Compute...............................................................................................20 5.4.2 Algoritmo bestSolution..........................................................................................21 5.4.3 Algoritmo fullPipelining........................................................................................24

6. Trabalhos Futuros..................................................................................................................268. Referências Bibliográficas....................................................................................................289. ANEXO I..............................................................................................................................29

9.1 ListaVizinhos()..............................................................................................................29 9.2 createHList()..................................................................................................................29 9.3 createNode()..................................................................................................................30 9.4 findFirstNode()..............................................................................................................30 9.6 isVisited()......................................................................................................................30 9.7 isListVisited()................................................................................................................31 9.8 getNextNode()...............................................................................................................31 9.9 getBusyProc()................................................................................................................31 9.10 getNextProc()..............................................................................................................31 9.11 isOnlyDepedency()......................................................................................................32 9.12 isDependent()..............................................................................................................32 9.13 isQueueEmpty()...........................................................................................................33 9.14 popQueue()..................................................................................................................33 9.15 pushQueue()................................................................................................................33 9.16 createQueueNode()......................................................................................................34 9.17 isOnQueue()................................................................................................................34 9.18 queueSize()..................................................................................................................34 9.19 removeElementFromQueue()......................................................................................34 9.20 printQueue()................................................................................................................35 9.21 findGodFather()...........................................................................................................35

7

Page 8: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

9.22 compute().....................................................................................................................36 9.23 bestSolution()..............................................................................................................39 9.24 readHList()..................................................................................................................43 9.25 printNodes().................................................................................................................43 9.26 fullPipelining()............................................................................................................44

8

Page 9: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

1.IntroduçãoCom o avanço sempre constante da tecnologia de hardware, a famosa “lei” de

Moore(Gordon Moore, 1929), proferida em abril de 1965:

“The complexity for minimum component costs has increased at a rate of roughly a factor of two per year ... Certainly over the short term this rate can be expected to continue, if not to increase” se manteve de certa forma verdadeira e correta.

Durante a última década, porém, a tecnologia chegou a um ponto onde, a quantidade de calor produzida por um processador era muito grande e o custo para que fosse dissipado inviabilizava o crescimento do ciclo de relógio dos processadores.

Para tentar contornar essa barreira, grandes empresas de hardware começaram a desenvolver CPU´s multi-processadas, fazendo com que instruções fossem processadas paralelamente. Essa nova tecnologia conseguiu, de certo modo, avançar sobre a antiga barreira, porém novos problemas foram criados.

A forma como a grande maioria dos softwares hoje em dia são desenvolvidos, utilizando-se o paradigma da programação serial, torna a computação paralela algo longe do ideal, pois a utilização completa das CPUs ainda é difícil, e durante um bom tempo de execução os núcleos ficam ociosos aguardando instruções provenientes dos outros núcleos.

É partindo desse problema que nossa pesquisa e nosso trabalho começam: de que forma podemos otimizar a utilização dos núcleos de um CPU, partindo de um programa serial?

Só que o problema não se atém somente ao mundo computacional. Em qualquer empresa que se utilize de processos de desenvolvimento de qualquer tarefa, a otimização dos recursos disponíveis também é necessária, e pode ser modelada da mesma maneira que o problema do paralelismo computacional. A modelagem é feita através de hipergrafos dirigidos, que serão definidos na primeira seção deste trabalho

Na segunda seção apresentamos algumas representações computacionais para hipergrafos dirigidos, e discorremos sobre a que utilizamos em nossos algoritmos.

O problema é de fato definido na terceira seção, bem como suas derivações e restrições impostas.

Com o problema definido e os conceitos apresentados, partimos para busca de sua solução. Na quarta seção, temos os algoritmos utilizados para a resolução em si dos problemas. Tentamos chegar a soluções ótimas, mas como mostraremos, nem sempre são possíveis.

9

Page 10: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

2. Conceitos fundamentais As seções abaixo trazem definições básicas todos os conceitos necessários para o pleno entendimento do trabalho como um todo.

2.1 Grafo e grafo direcionadoUm grafo, expresso frequentemente por G = (V,A), é composto por um conjunto não

vazio e finito V(conjunto dos vértices) e um conjunto A(conjunto das arestas) composto por pares não ordenados de elementos distintos de V, ou seja, cada elemento de A é um subconjunto de exatamente dois elementos de V. Como podemos observar no grafo da figura 01, temos que:

G = (V,A), sendo V = {1,2,3,4,5,6,7,8,9} e A = {{1,2},{1,3},{1,4},{1,5},{2,6},{2,7},{3,6},{4,7},{4,8},{4,5},{5,9},{7,8}}.

Um grafo direcionado D = (V, A) é, de forma análoga, composto por um conjunto não vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a” ϵ V x V. Um exemplo de grafo direcionado pode ser percebido na figura 02, onde nota-se claramente a direção das arestas, representada pelas setas em cada uma das arestas do grafo.

Representação de um Grafo (Figura 01) e um Grafo Direcionado (Figura 02)

2.2 Hipergrafo e hipergrafo direcionado

O hipergrafo é uma generalização do conceito de grafos. Em um hipergrafo a cardinalidade das arestas pode ser diferente de dois. Um hipergrafo H = (V,A) onde V é um conjunto finito de vértices e A é um conjunto finito de hiper-arestas, onde uma hiper-aresta “a” ϵ A é um subconjunto não vazio de V.

Cada hiper-aresta pode ligar dois ou mais vértices (na figura 03 temos, por exemplo, uma hiperaresta que parte dos vértices 1 e 2 e chega aos vértices 3 e 4). Essa característica nos possibilita representar computacionalmente diversas situações a fim de buscarmos soluções algorítmicas para as mesmas.

Avançando e especializando ainda mais o objeto de estudo, podemos definir hipergrafos dirigidos como sendo um par (H = (V,A)) onde o conjunto de vértices (V) é finito e o conjunto das hiper-arestas é formado por pares ordenados (X,Y), onde X e Y são subconjuntos disjuntos de elementos de V.

10

Page 11: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Aqui definimos que o conjunto arestas de saída S de um vértice 'v' é:

S(v) = {(X,Y) ϵ A | v ϵ X}

E como vértices de entrada:

E(v) = {(X,Y) ϵ A | v ϵ Y}

Percebemos, observando atentamente os hipergrafos das figuras 03 e 04, a diferença entre um hipergrafo e um hipergrafo direcionado: o conjunto de vértices é semelhante, porém ao utilizarmos hiperarestas direcionadas, definimos o caminho que se pode ser tomado dentro do hipergrafo.

Com esse conceito, podemos imaginar algumas situações em que hipergrafos direcionados se aplicam. Já faz algum tempo que cientistas da computação do mundo inteiro pesquisam sobre a programação paralela, e o conceito de hipergrafos direcionados aplica-se perfeitamente para as representações de programas que apresentem paralelismo. Imagine que podemos representar por um vértice 'u' de um hipergrafo direcionado um processo que conte com outros 'n' processos para atingir o resultado esperado na saída do programa. Podemos propor que para um hipergrafo H = (V,A) representando os processos de um programa, para cada 'v' ϵ V, temos que para cada aresta em E(v) temos um ou mais processos de quais 'v' depende e para cada aresta em S(v) temos uma ou mais processos que dependem de 'v'

Trazendo esse conceito para situações administrativas e de planejamento, podemos imaginar que cada vértice pode ser uma tarefa a ser executada e as hiper-arestas seriam dependências destas.

Com as informações de fluxo contidas nas hiper-arestas podemos calcular o número de processadores para a execução em menor tempo e otimizar a utilização dos recursos.

Há ainda a situação de instruções sendo jogadas no pipeline de um processador. O hipergrafo seria a modelagem da situação, sendo que cada vértice é uma instrução, e as hiper-arestas representam as dependências de dados. A otimização consiste em manter um dado dependente de outro o mais longe possível na execução, assim esta distância diminuiria o número de “bolhas” executadas pelo processador.

Em resumo, os hipergrafos direcionados “se apresentam como uma alternativa para a modelagem de problemas em que relações binárias usuais não são adequadas” (Guedes, Hipergrafos Direcionados – 2001).

Representação de um Hipergrafo (Figura 03) e de um Hipergrafo Direcionado (Figura 04)

11

Page 12: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

2.3 Ordenação Topológica

A ordenação topológica pode ser compreendida como uma forma de se ordenar os vértices de um grafo acíclico* afim de que, caso exista um caminho de um vértice v para um vértice u, então o vértice v aparecerá antes do vértice u nesta ordenação.

Representação de um Grafo com Ciclo (Figura 05)

Percebe-se que a ordenação topológica de um grafo(hipergrafo) qualquer não é única, e como já referenciado acima, ela não é aplicável em um grafo(hipergrafo) com ciclos, devido a interdependência dos vértices entre si.

A aplicação de ordenação topológica é comum a problemas de confecção de dicionários por exemplo, onde uma palavra qualquer word2 cuja definição dependa da palavra word1 apareça depois de word1 no dicionário.

Existem diversas maneiras para se obter a ordenação topológica de um grafo. Alguns algoritmos possuem uma complexidade linear (O(n)), porém outros mais lentos e ineficazes podem chegar a ter uma complexidade de (O(n³)).

Os primeiros algoritmos de enumeração topológica tinham como princípio os seguintes passos:

1º passo : Calcular o grau de entrada/saída de todos os vértices.

2º passo : Todos os vértices com menor grau de entrada são inseridos em uma lista (os primeiros a serem inseridos na lista são os com grau de entrada igual a zero)

3º passo : São removidos do grafo os vértices inseridos na lista do passo anterior.

4º passo : As arestas que saem destes vértices também são removidas.

5º passo : Os outros vértices têm seus graus diminuídos.

6º passo : Inicia-se o processo novamente até que o conjunto de vértices do grafo esteja vazio.

No nosso trabalho, durante a própria execução do algoritmo a ordenação topológica vai sendo feita, em tempo de execução, respeitando as dependências. A ordenação não é única, mas tentamos mantêr a forma mais conveniente para a solução do nosso problema. Apesar dos passos citados acima, a nossa ordenação topológica não os segue à risca.

*Acíclico : define-se um grafo como acíclico quando não encontramos ciclos dentro do grafo. Ciclo pode ser definido, de forma simples, como um caminho fechado sem vértices repetidos dentro de um grafo. Note que na figura 05 temos um ciclo formado pelo caminho {a,b,c,d,a}, porém existem outros ciclos dentro do grafo representado na figura.

12

Page 13: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

2.4 Filas

Uma fila nada mais é que uma estrutura FIFO(First-In-First-Out), ou seja, quando adicionado primeiro um item(ex. Um vértice do hipergrafo) q1 na fila, e depois outros n(q2 ... qn), o primeiro a ser retirado para a execução é q1, depois q2 e assim sucessivamente, obedecendo a exata ordem em que foram adicionados. Temos na figura 06 um exemplo de fila FIFO, onde o início da fila é apontado por Começo e o último elemento da fila é apontado por Fim.

Representação de uma Fila FiFo (Figura 06)

2.5 Pipeline

Pipelines são conjuntos de elementos que processam dados, conectados em série, sendo que a saída de um elemento é a entrada de outro. Pipelines são normalmente utilizados para aumentar o throughput de dados, ou seja, aumentar a vazão de dados em, por exemplo, um processador, não aumentando o tempo de execução de um dado sozinho.

2.6 Bolhas de Execução

Bolhas de execução são instruções adicionadas ao pipeline de um processador para preencher vazios de execução. Normalmente uma bolha é uma instrução que de fato executa nada, está lá só pra fazer com que o processador tenha o que executar, possivelmente esperando que algum dado fique pronto pra outra instução do pipeline. De fato, quanto menos bolhas tivermos em uma execução, temos um melhor desempenho. Situações ideais não apresentam bolhas.

3. Representação computacional Os grafos nos proporcionam soluções computacionais de forma a solucionarmos uma

gama muito grande de problemas. Mas, antes de pensarmos no algoritmo propriamente dito, devemos pensar em uma representação computacional para que o grafo possa ser trabalhado de forma simples.

Para mostrar as dificuldades encontradas no processamento computacional de hipergrafos, primeiro iremos mostrar as representações mais comuns para grafos e depois apresentaremos a representação adotada para os algoritmos expostos nesse trabalho.

13

Page 14: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Preferencialmente a representação deve utilizar estruturas simples de armazenamento em memória (vetores e matrizes), facilitando o tratamento. Para um grafo comum, os vértices podem ser representados por números para indexarmos as estruturas com o próprio nome do vértice e a estrutura pode ser uma matriz ou um vetor associado a um conjunto de listas ligadas.

3.1 Matriz de adjacênciasNa representação matricial(matriz de adjacências), utilizamos uma matriz |V|x|V|

denotada por A (pode-se notar que na figura 07, temos um grafo representado de 2 formas diferentes: utilizando uma matriz de adjacências e uma representação gráfica), onde se A(i,j) = 1, se {i, j} ϵ A e A(i,j) = 0 caso contrário. Essa representação ocupa um espaço em memória proporcional a |V|² bits, podendo ser diminuído à metade devido a sua simetria.

Representação de um grafo utilizando uma matriz de adjacências (Figura 07)

3.2 Lista de adjacências

Uma alternativa à representação matricial para grafos é a lista de adjacências.

A Lista de adjacências é um vetor adj [ ] indexado por {1, 2, ..., |V|} de listas ligadas. A lista adj [i] contém os vizinhos do vértice “i” e cada nó da lista é composto por “v” que armazena um vértice e prox que aponta para o próximo nó da lista. Tomando, por exemplo, o grafo da figura 08, percebemos que ele é facilmente representado através de uma lista de adjacências, a qual pode ser observada na figura 09. Temos um vetor representando a lista de vértices, e para cada vértice temos uma lista de arestas, onde cada elemento desta lista de arestas é composto por um par de valores, o primeiro representando o destino da aresta, e o segundo o peso desta aresta. Ao representarmos um grafo sem peso nas arestas, esse segundo valor torna-se descnecessário.

14

Page 15: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Representação de um Grafo de forma gráfica (Figura 08) e sua representação utilizando lista de Adjacências (Figura 09)

3.3 Representando HipergrafosA primeira dificuldade encontrada para a representação é a busca por estruturas

simples que possam nos proporcionar maneiras ágeis de pesquisa e percurso. Buscando esses dois fatores iremos trabalhar com quatro vetores de listas: BS, FS, org e dest.

A lista BS é um vetor indexado por {1, 2, ..., |V(G)|} de listas ligadas. A lista BS[i] contém as os hiper-arcos de chegada do vértice "i", ou seja, BS[i] armazena o conjunto E(i).

A lista FS é um vetor de estrutura igual a BS, com a diferença de representar o conjunto S(i) para o vértice "i", ou seja, armazena todos os hiper-arcos que saem do vértice "i".

As listas org e dest tem a função de armazenar o conjunto de vértices ligados por um hiper-arco. A lista org[i] contém os vértices de quais hiperarcos "i" sai(origem) e a lista dest[i] contém os vértices em quais "i" chega(destino).

Com essa estrutura definida podemos propor o seguinte algoritmo de percurso para exemplificar como a estrutura pode ser utilizada do hipergrafo seria feito. Utilizamos uma estrutura de armazenamento genérica L. Se L for uma fila, o algoritmo executará uma busca em largura, se for uma pilha, o algoritmo executará a busca em profundidade.

Algoritmo 1 Visite(G, u)

1: L = ;

2: enquanto (v novo)

3: marque v velho

4: para e ϵ FS(v)

5: L = L U dest[e]

6: enquanto (v velho & L ≠ )

7: v = remova de L

15

Page 16: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Algoritmo 2 Percurso(G)

1: para todo v ϵ V

2: marque v novo;

3: para todo v ϵ V

4: se (v novo)

5: Visite(G,u)

4. Problema propostoDentro do que já foi exposto, podemos dar a modelagem dos nossos problemas a

serem resolvidos.

4.1 Descrição do problemaEm várias áreas de conhecimento temos tarefas que são obtidas com a execução de

ações diversas, dependentes entre si. Utilizando hipergrafos iremos representar computacionalmente as ações e as dependências entre elas, ao mesmo tempo em que trabalharemos algoritmos para responder às seguintes questões:

1. Em quantos passos finalizaremos as ações com n unidades executoras?

2. Qual a ordem que as tarefas devem ser executadas?

3. Dado um hipergrafo, qual o número mínimo mínimo de unidades executoras para que as ações sejam executadas em tempo mínimo?

Essas três questões modelam nosso problema inicial, e com base nas mesmas foram projetados algoritmos para a uma possível resposta.

A maior preocupação é projetar algoritmos que executem em tempo polinomial, visto que a complexidade dos hipergrafos podem nos levar a algoritmos lentos e pouco objetivos e que, dependendo do tamanho e da forma do hipergrafo, podem não ter tempo útil para que uma resposta seja computada.

Outro problema abordado será uma conseqüência direta do algoritmo projetado para responder o problema supracitado, que é o problema das instruções dependentes em um pipeline de um processador.

A solução consiste em manter uma instrução longe o suficiente de sua dependência pra que seja executada sem “bolhas”.

16

Page 17: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

4.2 Restrições do problemaDada a complexidade da estrutura de representação, algumas restrições ao problema

devem ser impostas:

1. O hipergrafo não contém circuitos.Em um hipergrafo os vértices podem apresentar uma dependência circular. Em nossa

estrutura essa dependência representaria ações que apresentam alguma forma de retroalimentação.

Para que um circuito pudesse estar presente em nosso hipergrafo, teríamos que ter em nossa representação alguma forma de indicar a decisão de sair do circuito ou continuá-lo até certo ponto. Preferimos deixar de fora os circuitos. Na verdade com um certo tratamento no hipergrafo, o circuito pode até ser eliminado, mas não faz parte da abordagem do nosso trabalho.

2. O hipergrafo apresenta somente um vértice inicial.Adotamos como vértices iniciais os vértices do hipergrafo que não apresentem

dependências no início do algoritmo de tratamento.

Nosso algoritmo não propõe o tratamento das tarefas que tenham mais de uma alternativa de início, pois esse problema pode ser contornado com a inserção de um vértice de qual todos os vértices iniciais sejam dependentes. Novamente, foi uma escolha de implementação. A forma como o arquivo de entrada é construído não cabe a nós decidir, só padronizamos como o arquivo deve estar.

2.1 Qualquer número de vértices iniciais, no problema do pipeline.No algoritmo de otimização de saída do pipeline, consideramos quantos

vértices sem “pai” existem no hipergrafo, não valendo a restrição 2 nesse caso. Estará explícito na explicação do algoritmo.

5. ImplementaçãoExpostos o problema e as definições vamos agora projetar algoritmos que nos

forneçam as respostas para as questões expostas. Optamos pela linguagem c para a implementação, por ser de certa forma mais pura e eficiente, não nos valendo de recursos da linguagem para a implementação. A escolha da linguagem reflete também a preocupação com a otimização da execução, sendo que seria uma boa escolha para sistemas embarcados, por exemplo.

Em primeiro lugar, descreveremos como foi implementada a estrutura de armazanamento definida, e após vamos expôr os algoritmos.

5.1 Estrutura de representação do hipergrafoComo visto na definição a estrutura de armazanamento proposta é composta de quatro

listas. Para definir essas listas utilizaremos a seguinte estrutura:

Estrutura 1 - Listas

1: typedef struct{

17

Page 18: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

2: int *nodes;

3: int total;

4: } hyperstruct

Definimos essa estrutura como tipo atribuindo-a um nome comum, sendo que essa será usada para a representação das listas BS, FS, org e dest.

Nessa estrutura armazenaremos um vetor de inteiros, onde cada posição contém o número correspondente ao vértice ou a aresta, e um total para facilitar a verificação do tamanho da lista, possibilitando comparações com um acesso entre vértices ou arestas distintas.

Com o armazenamento das listas definido, definimos uma segunda estrutura para o armazenamento do grafo por completo, centralizando nossa representação completa nessa estrutura:

Estrutura 2 - Hypergrafo

1: typedef struct{

2: int totalNodes;

3: int totalEdges;

4: hyperstruct *org;

5: hyperstruct *dest;

6: hyperstruct *BS;

7: hyperstruct *FS;

8: } hypergraph

5.2 Estruturas auxiliaresPara que possamos simular a execução das ações com os algoritmos iremos precisar

de uma estrutura auxiliar onde podemos armazenar os vértices, como por exemplo, os vértices cujas dependencias já foram executadas. Uma simples lista encadeada já nos permite a representação, e cada nó da lista será uma estrutura com um inteiro, representando o vértice, e um ponteiro para essa mesma estrutura, que aponta para o próximo item, ou para null se esse nó for o último da lista.

Estrutura 2 - Hypergrafo

1: typedef struct{

2: int value;

18

Page 19: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

3: queue *prox;

4: } queue

5.3 Definição da estrutura do arquivo de leitura do HipergrafoApós definirmos as estruturas utilizadas, as estruturas auxiliares, e as funções que

utilizamos para implementar as resoluções dos problemas, se faz necessário definir como foi implementada a estrutura do arquivo de entrada para a leitura do hipergrafo utilizado pelo programa.

Tomando um arquivo-exemplo, temos:

3 Número de Hiper-Arestas 0 Lista de vértices de origem da primeira hiper-aresta 2 Lista de vértices de destino da primeira hiper-aresta 1 2 Lista de vértices de origem da segunda hiper-aresta 3 4 Lista de vértices de destino da segunda hiper-aresta 1 3 Lista de vértices de origem da terceira hiper-aresta 5 Lista de vértices de destino da terceira hiper-aresta 6 Número de Hiper-nodos - Lista de hiper-arestas que chegam no primeiro vértice 0 Lista de hiper-arestas que saem do primeiro vértice - Lista de hiper-arestas que chegam no segundo vértice 1 2 Lista de hiper-arestas que saem do segundo vértice 0 Lista de hiper-arestas que chegam no terceiro vértice 1 Lista de hiper-arestas que saem no terceiro vértice 1 Lista de hiper-arestas que chegam no quarto vértice 2 Lista de hiper-arestas que saem do quarto vértice 1 Lista de hiper-arestas que chegam no quinto vértice - Lista de hiper-arestas que saem do quinto vértice 2 Lista de hiper-arestas que chegam no sexto vértice - Lista de hiper-arestas que saem do sexto vértice

A primeira linha do arquivo define quantas hiper-arestas temos dentro do hipergrafo. A Seguir, Temos sempre um par de linhas para cada hiper-aresta, a primeira lista do par define os nós de origem da hiper-aresta, e a outra linha define os nós de destino da hiper-aresta. Ou seja, uma hiper-aresta pode ser definida, de forma mais simples:

ha = [(hnA,hnB,...,hnM),(hnA',hnB',....,hnN)], sendo que:

(hnA,hnB,...,hnM) = conjunto de hiper-vértices que definem a a origem da hiper-aresta.

(hnA',hnB',...,hnN) = conjunto de hiper-vértices que definem o destino da hiper-aresta.

É de fácil compreensão que, se um hipergrafo possui n hiper-arestas, ele terá 2n linhas que definem as hiper-arestas.

Após definir as hiper-arestas, temos uma linha que define a quantidade de hiper-nodos

19

Page 20: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

do hipergrafo, que são definidos nas linhas seguintes, da mesma forma que as hiper-arestas são definidas. Assim, temos um par de linhas para cada nó, sendo que a primeira linha do par define quais hiper-arestas chegam no vértice, e a segunda linha define quais hiper-arestas partem do nodo.

De modo mais simples, temos:

hn = [(haA,haB,...,haM),(haA',haB',...haN)], sendo que:

(haA,haB,...haM) = conjunto de hiper-arestas que chegam em hn.(haA',haB',...haN) = conjunto de hiper-arestas que partem de hn.

Do mesmo modo que para as hiper-arestas, temos que para n hiper-vértices, teremos 2n linhas dentro do arquivo de entrada que definem os hiper-vértices.

5.4 Algoritmos para Cálculo das SoluçõesTendo definido como foi estruturado por nós o hipergrafo, com suas 4 filas

(FS,BS,Dest e Org), como o arquivo de entrada do programa que representa o hipergrafo a ser trabalho foi definido, começamos agora a definição dos principais algoritmos que propomos para a solução dos problemas propostos na seção 3.

A seguir definiremos os principais algoritmos propostos, utilizando pseudo-código para melhor compreensão e um representação mais alto nível.

Lembramos que as implementações em linguagem C encontram-se na seção anexo I.

5.4.1 Algoritmo Compute

Compute(hipergrafo hg, numProcs k)1: Para todo( v ϵ Hipergrafo) 2: Marque v “não visitado”;3: Para todo (k ϵ Conj.Processadores)4: Marque k livre;5: Enquanto(existeNaoVisitado) 6: Se ( !ExisteDependencia(v ϵ filaExecucao) )7: Enquanto ( (PegaProcessadorLivre(proc)) E (ListaExecucao.tamanho > 0) )8: Marque v “visitado”;9: Aloque v para proc;10: Retire v de ListaExecucao;11: Incremente passo;12: Enquanto((PegaProcessadorOcupado(proc))13: Para todo (v' alcançavel por v)14: Se (!ExisteDependencia(v')15: Se(v' não pertence FilaExecução)

20

Page 21: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

16: Insere v' em FilaExecução;17: Senão18: Vértice v' não está pronto;19: Marque proc “livre”;20: Imprime Status(FilaExecução);

A idéia deste algoritmo é receber um hipergrafo hg e um número de unidades processadoras k e, utilizando-se de um vetor indexado pelos índices dos hiper-nodos, visitá-los todos, distribuindo a execução entre os k processadores passados como argumento.

Fica claro que se todos os hiper-nodos foram visitados, a execução deve parar. Mas a execução acaba também quando a fila fica vazia(linha 5). Isso ocorre porque, pra todo hiper-nodo colocado na Fila de Execução, são calculadas suas dependências e, caso não estejam na fila, são adicionadas a esta também(linhas 14, 15 e 16). Logo após esse passo, é retirado o hiper-nodo pai de execução. Com isso, fica garantido que uma fila só fica vazia se nenhum hiper-nodo tem mais dependências, chegando assim ao fim do hipergrafo.

Sempre que um hiper-nodo está para ser removido da Fila de Execução, verifica-se a existência de uma unidade processadora livre para que este hiper-nodo possa de fato ser executado. No nosso caso, sempre temos unidades livres, pois na linha 19 setamos os processadores como livres, e isso ocorre ao fim de cada loop. Mas a implementação da verificação se há uma unidade processadora livre ocorreu para que pudéssemos utilizar hiper-nodos com pesos diferentes, assim, ocupariam tempos diferentes em cada processador, só que esta implementação ficou para os Trabalhos Futuros, seção esta que pode ser vista no final deste trabalho.

A maneira como o próximo hiper-nodo é escolhida(sempre os “pais” devem ter sido executados para que um “filho” possa entrar em execução) é a própria ordenação topológica do hipergrafo.

Ao final da execução deste algoritmo, temos o número de passos que foram necessários para a execução completa do hipergrafo(passos contados na linha 11).

Também temos que, o grau de paralelismo do hipergrafo é dado pelo tamanho máximo da fila em algum ponto da execução(como explicado no começo desta seção), portanto, mais uma pergunta respondida. Só que, apesar deste algoritmo funcionar e responder às questões, percebemos que com algumas melhorias, poderíamos diminuir o número de passos de execução, ou ainda, manter o mesmo número de passos, mas diminuindo o grau de paralelismo.

Isso nos levou ao segundo algoritmo, descrito a seguir.

5.4.2 Algoritmo bestSolution

bestSolution(hipergrafo hg, numProcs k)1: Para todo( v ϵ Hipergrafo) 2: Marque v “não visitado”;3: Para todo (k ϵ Conj.Processadores)

21

Page 22: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

4: Marque k livre;5: Enquanto(existeNaoVisitado) 6: Fila qa recebe fila q1;7: Enquanto (tamanho(qa) > 0)8: Para todo (v ϵ qa)9: Para todo(v' alcançável por v)10: Se(!ExisteDependencia(v'))11: Insira v' na Fila q2;12: Se(tamanho(q2) > 0) 13: Fila qa recebe nodos não dependentes da Fila q1 e q2;14: Enquanto((tamanho(q1) > tamanho(q2)) E (q2 !vazio))15: Concatena Fila q2 com Fila qa; 16: Retira nodos de qa da Fila q1;17: Retira nodo da fila qa;18: Se (k < 0)19: Enquanto(q1 !vazio OU L < k ) 20: Visita(v' ϵ q1);21: Retira v' da Fila q1;22: Incrementa L;23: Fila q1 recebe Fila q2;24: Fila q2 recebe Fila vazia;25: Senão26: Enquanto(q1 !vazio && (PegaProcessadorLivre(proc)))27: Visita(v' ϵ q1);28: Retira v' da Fila q1;29: Fila qa recebe Fila q1; 30: Enquanto(qa !vazio)31: Fila qb recebe Fila q2;32: Enquanto(qb !vazio)33: Se(v' ϵ qb for depedente de v ϵ qa)34: Fila temporaria recebe Fila qb sem o vértice v';35: Para Todo(v” ϵ qb)36: Retira v” da Fila q2;37: Fila qb recebe Fila temporária;38: Senão39: Retira da fila qb o vértice v';

22

Page 23: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

40: Retira da fila qa o vértice v;41: Fila qa recebe Fila q1;42: Se (qa !vazio)43: Concatena Fila qa com Fila q2; 44: Senão 45: Fila q1 recebe Fila q2;46: Fila q2 recebe fila vazia;47: Incrementa Passo;

O nome bestSolution indica que talvez devesse ser a melhor solução, mas como mostraremos, a melhor solução pode não existir.

A idéia geral desse algoritmo é quase a mesma que do citado em 5.4.1. Distribuir as tarefas(hiper-nodos) entre um número k de unidades processadoras. Aqui utiliza-se também o conceito da fila de execução para manter os hiper-nodos prontos pra execução, mas nesse caso, utilizamos duas filas.

Escolhemos o uso do conceito de duas filas(conceito pois a implementação pode ser um vetor somente, com um pivô) pois assim conseguimos otimizar o uso das unidades processadoras.

Funciona assim: inicialmente colocamos o hiper-nodo inicial em uma fila q1. Iniciamos a execução calculando quais os hiper-nodos serão liberados com a execução do(s) hiper-nodo(s) da fila q1 – não os executando – e os colocamos em outra fila, a q2. Até esse ponto, ninguém foi executado, então é como se estivéssemos um passo adiantado na execução. Essa técnica nos permite reorganizar os hiper-nodos que serão executados de tal forma que otimize o uso das unidades processadoras.

Sempre que um hiper-nodo da fila q1 for candidato à execução, analisamos se ele é pai de alguém presente em q2. Caso não, significa que não tem dependência com nenhum outro hiper-nodo, e pode ter sua execução retardada, ou seja, pode ser passado pra q2. A vantagem de se poder fazer isso, é que, imagine que em algum ponto da execução temos 6 hiper-nodos prontos em q1, e somente 1 em q2. Se tivéssemos 4 unidades processadoras, teríamos que executar(sem otimização) 4 hiper-nodos em q1, os 2 restantes de q1, pra depois executarmos o único em q2. Teríamos 3 passos de execução, sendo que no segundo passo teríamos 2 unidades não utilizadas, e no terceiro passo, 3 unidades não utilizadas. Agora, com o nosso conceito aplicado no algoritmo, se dos 6 hiper-nodos iniciais em q1, pelo menos 2 puderem ter a execução retardada, teríamos a execução em somente 2 passos: 4 hiper-nodos no primeiro, e 3 no segundo. Além de diminuir o número de passos, mantivemos quase todos os processadores disponíveis ocupados, na maioria do tempo.

Nesse algoritmo, movemos os hiper-nodos possíveis até que as filas q1 e q2 fiquem do mesmo tamanho, ou com 1 unidade de diferença. É fácil perceber o porquê dessa escolha: o tamanho de q1(que é a fila em execução sempre) definirá o grau de paralelismo do hipergrafo. Se pudermos mover hiper-nodos até que as filas se igualem, o grau estará mantido.

Obviamente há a opção de sempre atrasar a execução dos hiper-nodos que possam ter a execução atrasada, até que sejam estritamente necessários. Inicialmente parece uma boa idéia, e muitas vezes o é, mas veja que podemos atrasar demais a execução de muitos hiper-nodos, e num pior caso todos sejam necessários para a execução. Nesse caso, o grau de paralelismo pode ser aumentado facilmente, pois teremos muitos nós na fila de execução,

23

Page 24: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

necessários a serem executados, ao mesmo tempo. Optamos por não utilizar esta abordagem, uma vez que preferimos garantir um certo grau médio, a cair em casos como este.

No mais, a execução do algoritmo é exatamente como o Compute(5.4.1). Checam-se as dependências de um hiper-nodo antes de tirá-lo da fila de execução, adiciona-se estas à fila, e prossegue-se executando o algoritmo, até que todos estejam visitados, ou a fila vazia.

Como supracitado, podemos não ter um algoritmo ótimo, visto que nem sempre atrasar a execução de tarefas que podem ser postergadas resulta em melhora no desempenho.

5.4.3 Algoritmo fullPipeliningO algoritmo fullPipelining é consequência direta dos dois algoritmos anteriores, mas

sem usar unidades processadoras. A idéia é dar uma seqüência de hiper-nodos(que podem ser vistos como instruções Assembly entrando no pipeline de um processador), de modo que a execução de um hiper-nodo pai fique o mais distante possível de seu filho.

Essa distância é para evitar que sejam colocadas bolhas no pipeline, afim de esperar algum dado ficar pronto. Se conseguirmos manter a distância entre esses dois hiper-nodos grande o suficiente para que nunca coloquemos bolhas, é a situação ideal. Por isso, são realocados os hiper-nodos de maneira a se intercalarem, assim mantemos sempre um pipeline cheio de instruções úteis, aumentando o throughput.

Para esse algoritmo, tiramos algumas exceções que haviam sido estabelecidas. Primeiramente, podemos ter vários hiper-nodos iniciais. Além do abandono de algumas regras, temos que descartar a idéia das duas filas utilizada no algoritmo para otimização de tarefas, pois quando atrasamos a execução de um hiper-nodo para executá-lo somente quando dependências forem detectadas, aproximamos esse hiper-nodo de seu filho, gerando obrigatoriamente bolhas entre a execução desse pai e de seu filho.

A solução proposta abandonará a idéia de processamento paralelo na visão de múltiplos executores. O pensamento agora é a busca de uma solução para somente uma unidade executora que apresente pipeline. Essa solução buscará formar uma fila de execução de instruções para que o pipeline dessa unidade executora seja maximizado.

Temos aqui a solução ideal como sendo aquela que, a partir do momento que o pipeline estiver cheio (todas as fases de execução com uma instrução), nenhuma bolha seja gerada. Fica fácil observar que, por exemplo, para um pipeline de profundidade cinco, se conseguirmos organizar essa fila com uma distância mínima também cinco, desconsiderando as primeiras execuções, atingiremos o ideal.

Para isso, a idéia aqui apresentada não pretende somente maximizar a média das distâncias, mas também equilibrar a distância entre um hiper-nodo do seu pai e do seu primeiro filho executado. Isso porque, como mostraremos no exemplo a seguir, a média não demonstra a otimização efetuada. Consideraremos nessa seção o pai de um hiper-nodo aquele que liberou esse hiper-nodo para ser executado, ou seja, o último pai do hiper-grafo que foi executado.

Para iniciar a explicação do algoritmo proposto, consideremos a seguinte figura representando uma fila arbitrária de execução sem a otimização proposta.

24

Page 25: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Figura 10

Representação de uma fila de execução sem otimização

Na figura acima observamos a execução de uma fila sem otimização. Cada quadrado representa uma instrução, sendo que os mais escuros apresentam dependência. Se calcularmos a média de distância das instruções que apresentam dependências teremos como resultado cinco. Mas se o pipeline for o mesmo citado anteriormente, de tamanho cinco, a execução apresentará ao menos quatro bolhas entre a execução da instrução 2 seu filho (instrução 3).

Para otimizar essa execução, podemos mover hiper-nodo 2 ou 3 para maximizar essa distância. Movendo o terceiro hiper-nodo, estaremos atrasando a sua execução e o aproximando do filho. Como já justificado no descarte da solução das duas filas, isso não é desejável.

Resta-nos trabalhar com o hiper-nodo 2. E para equilibrar as distâncias, basta movermos esse hiper-nodo para uma distância intermediária.

Figura 11

Representação da fila de execução da figura 10 com otimização

Com a otimização a média inicial não foi alterada porém, considerando as instruções escuras, no pipeline de profundidade cinco, nenhuma bolha foi criada.

No nosso algoritmo aplicaremos a mesma idéia: moveremos o pai do hiper-nodo a ser executado para a posição intermediária entre o pai do pai e esse hiper-nodo. Analisaremos a cada início de execução de instrução a situação da fila e aplicaremos a regra acima, tomando os devidos cuidados para que o nó movimentado não ultrapasse um filho.

Estabelecidos os princípios do algoritmo, o algoritmo pode ser construído utilizando:

● um contador cont, armazenando o passo atual da execução;

● uma fila q armazenando os hiper-nodos livres para serem executados;

● um vetor executed indexado pelo hiper-nodo, indicando se já foi executado;

● um vetor passOfExecution indexado pelo hiper-nodo, indicando em que passo o hiper-nodo foi executado;

● um vetor execution indexado pelo passo, indicando que hiper-nodo foi executado no passo;

● um vetor dist indexado pelo hiper-nodo, armazenando a distância para o pai;

● um vetor fathers indexado pelo hiper-nodo, armazenando qual o pai do hiper-nodo;

25

Page 26: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

fullPipelining(hipergrafo hg)1: Para todo( v ϵ V)2: dist[v] = 03: executed[v] = false4: Insere nós sem dependência em q5: cont = 06: Enquanto (ExisteHiper-nodoNãoExecutado)7: v = remova de q8: Insere em q todos os vértices liberados por v9: executed[v] = true10: passOfExecution[v] = cont;11: execution[cont] = v12: Se (NãoExistePai(v)) //para os primeiros hiper-nós13: fathers[v] = -1;14: dist[v] = 0;15: Senão16: fathers[v] = ProcuraPai[v]17: dist[v] = cont - passOfExecution[fathers[v]]18: aux = cont - (dist[fathers[v]] + dist[v])/2;19: execution[aux] = fathers[v]20: Atualiza passOfExecution21: Atualiza dist22: cont++

Aplicamos o fullPipelining também ao algoritmo 5.4.1 afim de podermos comparar os resultados.

6. Trabalhos Futuros– O algoritmo das duas filas atingiu resultados bastante positivos, portanto, em próximos

trabalhos vamos tentar melhorar seu desempenho, talvez utilizando um meio termo entre sempre atrasar a execução, e atrasar uma vez só.

– O algoritmo fullPipelining pode talvez ser melhorado utilizando técnicas estatísticas, como calcular distâncias médias e mover hiper-nodos baseados nestas distâncias(inclusive o hiper-nodo pai, que neste trabalho nunca é movido). Pra isso, seria necessário pelo menos uma passada inicial no hipergrafo para que um conjunto de execução fosse criado.

26

Page 27: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

7. Conclusão

Mostramos um algoritmo que distribui processos de um hipergrafo para k recursos(ex. processadores) de forma linear, ou seja, sem nenhuma otimização, e outro algoritmo otimizado utilizando-se o conceito de duas filas apresentado aqui, em que tentamos adiantar a execução dos próximos hipernós em um passo, para que assim pudéssemos otimizar sua execução como um todo.

Vimos que de fato esse algoritmo funciona. Na maioria dos casos consegue-se diminuir o número de passos em uma execução com o mesmo número de processadores, e ainda mantê-los mais ocupados que um algoritmo normal. Ainda mostramos que nosso algoritmo consegue executar todo um hipergrafo com o mesmo número de passos que um algoritmo padrão, só que utilizando menos processadores.

Essas otimizações são claramente importantes no que concerne a computação paralela, visto que a ociosidade de processadores é diminuída, não sub-utilizando o sistema. No caso de processos administrativos, o melhor uso dos recursos envolvidos significa economia real de tempo e dinheiro.

Considerando o hipergrafo como sendo instruções vindas de processadores e suas dependências de dados, apresentamos um algoritmo que tenta manter a execução de um hipernó pai e um hipernó filho o mais distante possível. Fizemos isso pois, entre essa distância desses dois nós, o processador pode se ocupar com outras instruções(encher o pipeline), e num caso ideal, nunca precisa ficar ocioso(gerar as conhecidas “bolhas”).

Com esse trabalho, tentamos buscar as soluções ideais, mas nem sempre elas existem para esse problema. Ficou claro na execução do algoritmo das duas filas, em que preferimos utilizar uma abordagem de manter as filas com o mesmo tamanho, do que mandar sempre que possível a execução de um nó para o próximo passo. Nem sempre atrasar a execução de um nó se mostra melhor que executá-lo no passo atual, e vice-versa. Nesse caso, não há abordagem ótima, visto que a entrada não é conhecida, e a distribuição das tarefas é feita em tempo de execução.

27

Page 28: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

8. Referências Bibliográficas

[1] Guedes, A.L.P. Hipergrafos Direcionados. Tese de Doutorado pela Universidade Federal do Rio de Janeiro, Agosto 2001

[2] Paola Alimonti, Esteban Feuerstein. Petri Nets, Hypergraphs and Conflicts. In 18th International Workshop on Graph-Theoretic Concepts in Computer Science (WG'92), pages 293-309, Germany, June 1992

[3] Giorgio Ausiello, Alessandro D'Atri e Domenico Saccà. Graph Algorithms for Functional Dependency Manipulation. Journal of the Association for Computer Machinery, 30(4):752-766, October 1983

[4] Radhakrishnan Sridhar e Sitharama S. Iyengar. Efficient Parallel Algorithms for Functional Dependency Manipulations. Louisiana State University, 1990

[5] Ernst W. Mayr. Well Structured Parallel Programs Are Not Easier to Schedule. Stanford University, September 1981

[6] Murata, Tadao. Petri Nets: Properties, Analysis and Applications. Proceedings of the IEEE, 77(4):541-580, April 1989

[7] Karen D. Devine, Erik G. Boman, Robert T. Heaphy, Rob H. Bisseling e Umit V. Catalyurek. Parallel Hypergraph Partitioning for Scientific Computing. Sandia National Laboratories Dept. Of Discrete Algorithms and Math, Utrecht University Dept. Of Mathematics, Ohio State University Dept. Of Biomedical Informatics

[8] Trifunovic, Aleksandar. Parallel Algorithms for Hypergraph Partitioning. Tese de Doutorado pela University of London, Imperial College of Science, Technology and Medicine, February 2006

28

Page 29: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

9. ANEXO I

9.1 ListaVizinhos()Dada a simplicidade dessa representação podemos imaginar um algoritmo de listagem

de um vértice v para exemplificar como trabalhar com esta:

Algoritmo 3 – ListaVizinhos(int v, hipergraph G) {

1: for(i=0; i < G.FS[v].total; i++){

2: for(x=0; x < G.dest[i].total; x++){

3: printf(“%d,”,G.dest.nodes[x]);

4: }

5: }}

Apesar da simplicidade de percurso, detectamos que a simples pesquisa de vizinhos de um vértice do hipergrafo pode ter um custo de O(|E|*|V|). Mas para os fins da proposta (série de ações para executar uma tarefa) uma representação de dependencia dupla entre os vértices se torna desnecessária. Sendo assim o algoritmo pode ser executado em O(|V|), pois se uma aresta que sai de um vertice v contém o vértice u, u não estará presente em outras arestas que saem de v.

Além das estruturas definidas anteriormente, fez-se necessária a criação de algumas funções que trabalhassem sobre essas estruturas, afim de que os resultados esperados fossem alcançado da forma mais satisfatória possível. Como veremos a seguir, as funções serão apresentadas e rapidamente explicadas para um claro entendimento e uma compreensão mais apurada do que se pretende apresentar.

9.2 createHList()A função *createHList recebe como parâmetro um inteiro, o qual define o tamanho da

lista de hyperstructs a ser retornada. Cada hyperstruct (list[i].total) recebe como tamanho total o valor inicial de 0, por não possuir no momento da criação nenhum nodo setado.1: hyperstruct *createHList(int size){2: hyperstruct *list;3: int i;4: if((list = (hyperstruct *)malloc(size * sizeof(hyperstruct))) == NULL){5: puts("Error: Unable to allocate memory");6: exit(1);7: }8: for(i = 0; i < size; i++) list[i].total = 0;9: return(list);10: }

29

Page 30: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

9.3 createNode()A função createNode recebe uma hyperstruct e um inteiro como parâmetros. A lista

de nodos da hypestruct é realocada de uma posição, assim sempre teremos a última posição livre. Após expandirmos a lista de nodos, inserimos o valor dentro da primeira posição livre da lista (ha.total – 1) e retornamos a hyperstruct com o nodo inserido.

hyperstruct createNode(hyperstruct ha, int value){ if((ha.nodes = (int *)realloc(ha.nodes, ++ha.total * sizeof(int))) == NULL){ puts("Error: Unable to reallocate memory"); exit(1); } ha.nodes[ha.total - 1] = value; if(value < 0) ha.total = 0; return (ha);}

9.4 findFirstNode()A Função findFirstNode, como o próprio nome sugere, recebe uma hypestruct e um

limitador de tamanho como parâmetros, e retorna o índice do primeiro nodo livre existente dentro da hypestruct. Caso não exista uma hyperstruct livre dentro da list, isto é, uma hypestruct que não possua nodos, a função retornará -1. O que a função faz é varrer a lista de hyperstructs atrás do primeiro hypestruct vazio, caso a última posição tenha sido atingida (posição essa definida pelo limitador total) a função retorna a falha em encontrar a hyperstruct livre, retornando -1.

int findFirstNode(hyperstruct *hs, int total){ int i = 0;

while(i < total){ if(hs[i].total == 0) return(i); i++; }

return(-1);}

9.6 isVisited()A função isVisited recebe a lista de vértices, com suas respectivas flags de visitado ou

não, além de o delimitador do número de nodos da lista. Caso exista algum nodo não visitado, a função retorna false, caso todos os nodos tenham sido visitados, a função retornará true.

int isVisited(int visited[], int n){int i = 0;while(i < n){

if(visited[i] == 0) return(0);i++;

}return(1);

}

30

Page 31: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

9.7 isListVisited()A função isListVisited recebe um hipergrafo, a lista de vértices visitados e o nodo a ser

testado. O que a função faz é testar se todos os pais do nodo já foram visitados, caso algum de seus pais não tenha sido visitado a função retorna false, caso contrário a função retornará true.

int isListVisited(hypergraph hg, int visited[], int node){int i = 0;int j = 0;

for(i = 0; i < hg.BS[node].total; i++){for(j = 0; j < hg.org[hg.BS[node].nodes[i]].total; j++){

if(visited[hg.org[hg.BS[node].nodes[i]].nodes[j]] == 0) return(0);}

}

return(1);}

9.8 getNextNode()A função getNextNode recebe a lista de processos a serem executados e uma

hyperstruct como parâmetros. Busca dentro do vetor visited o primeiro processo aguardando ser visitado (visited[i] = 0). Caso exista algum processo ainda a ser visitado, a função retorna o índice desse processo, caso contrário retornará -1.

int getNextNode(int visited[], hyperstruct hs){int i = 0;

while(i < hs.total){if(visited[hs.nodes[i]] == 0) return(hs.nodes[i]);i++;

}return (-1);

}

9.9 getBusyProc()A função getBusyProc recebe uma lista de processadores, um inteiro k que limita o

numero máximo de processadores da lista. A função testa a lista do processadores, caso algum esteja vazio, o número(índice) desse processador é retornado. Caso todos os processadores estejam livre, a função retorna -1

int getBusyProc(int procs[], int k){int i;

for(i = 0; i< k; i++) if(procs[i] != -1) return(i);return(-1);

}

9.10 getNextProc()A Função getNextProc recebe a lista de processadores, o tamanho da lista e retorna o

primeiro processador livre existente dentro da lista. Caso nenhum esteja livre a função retornará -1.

31

Page 32: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

int getNextProc(int procs[], int k){ int i;

for(i = 0; i < k; i++) if(procs[i] == -1) return(i); return(-1);}

9.11 isOnlyDepedency()A Função isOnlyDependency busca as dependências do vértice. A Função busca todos

os vértices dos quais o vértice nodeDependent é dependente. A cada vértice pai, a função testa se ele está setado como já visitado e se ele está na fila de execução. Caso algum vértice pai não tenha sido visitado ou ele ainda esteja na fila de execuções a função retornará false. Caso todos os vértices pai já tenham sido visitados e não estejam na fila de execução (ou seja, já foram visitados E executados) a função retornará true, alertando que o vértice em questão está pronto para ser executado.

int isOnlyDependency(hypergraph hg, int nodeDependent, int *visited, queue *q){ int i,j; int edge,node;

for (i=0; i < hg.BS[nodeDependent].total; i++){ edge = hg.BS[nodeDependent].nodes[i]; for (j=0; j < hg.org[edge].total; j++){ node = hg.org[edge].nodes[j]; if (!visited[node] && !isOnQueue(q, node)){ return(0); } } }

return(1);}

9.12 isDependent()A função isDependent testa se o vértice nodeDependent é dependente do vértice

nodeFather. Varrendo todas as dependências do vértice nodeDependent, a função testa cada um dos vértices pai comparando com o vértice nodeFather.

Caso o nodo nodeFather for pai do nodo nodeDependent a função retornará true, caso contrário retornará false.

int isDependent(hypergraph hg, int nodeDependent, int nodeFather){ int i,j; int edge,node; for (i=0; i < hg.BS[nodeDependent].total; i++){ edge = hg.BS[nodeDependent].nodes[i]; for (j=0; j < hg.org[edge].total; j++){ node = hg.org[edge].nodes[j]; if (node == nodeFather){ return(1); } } } return(0);}

32

Page 33: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

A função isDependent testa se o vértice nodeDependent é dependente do vértice nodeFather. Varrendo todas as dependências do vértice nodeDependent, a função testa cada um dos vértices pai comparando com o vértice nodeFather.

Caso o nodo nodeFather for pai do nodo nodeDependent a função retornará true, caso contrário retornará false.

9.13 isQueueEmpty()A Função isQueueEmpty recebe a lista de execução q e retorna true caso ela esteja

vazia, caso contrário retornará false.

int isQueueEmpty(queue *q){ if(q == NULL) return(1); return(0);}

9.14 popQueue()A Função popQueue retira o primeiro nodo da lista q, corrije os ponteiros necessários

(desenfila o nodo n, e faz o ponteiro *q apontar para q->prox) e retorna a lista corrigida.

queue *popQueue(queue *q){ queue *n;

if(q != NULL) { if(q->prox == NULL) { free(q); return(NULL); } n = q; q = q->prox; free(n); return(q); } else return(NULL);}

9.15 pushQueue()A Função pushQueue recebe a lista q e o valor a ser inserido na lista value. Assim, a

função insere o nodo com valor igual a value no fim da lista e retorna a lista atualizada.

queue *pushQueue(queue *q, int value){ queue *f = q; queue *n;

if(q == NULL) { n = createQueueNode(value); return(n); }

while(q->prox != NULL) q = q->prox; n = createQueueNode(value);

q->prox = n;

return(f);}

33

Page 34: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

9.16 createQueueNode()A Função createQueueNode cria um nodo no final da lista, que recebe como valor o

parâmetro value. Após a inserção a função retorna a lista atualizada.

queue *createQueueNode(int value){ queue *q;

if((q = (queue *)malloc(sizeof(queue))) == NULL){ puts("Error: Unable to allocate memory"); exit(1); }

q->value = value; q->prox = NULL;

return(q);}

9.17 isOnQueue()A Função isOnQueue recebe uma lista de execução e um nodo, testa se o nodo passado

como parâmetro está na lista q. Caso esteja dentro de q a função retornará true, caso contrário retornará false.

int isOnQueue(queue *q, int value){ queue *f = q;

while(q != NULL){ if(q->value == value) return(1); q = q->prox; } return(0);}

9.18 queueSize()A Função queueSize recebe uma lista de execução q e retorna o tamanho da lista.

int queueSize(queue *q){ queue *f = q; int i = 0;

while(q != NULL){ q = q->prox; i++; } return(i);}

9.19 removeElementFromQueue()A Função removeElementFromQueue recebe uma lista q e um elemento de lista elem.

Caso elem faça parte da lista q, ele é removido, e a lista atualizada é corrijida, ou seja, os ponteiros de prox são corrijidos e a memória referente ao elem que foi removido é liberada.

34

Page 35: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

queue *removeElementFromQueue(queue *q, queue elem){ queue *anterior = NULL; queue *result = NULL; result = q;

if (q == NULL) return(NULL); if (q->value == elem.value){ anterior = q; q = q->prox; free(anterior); return(q); } anterior = q; q = q->prox;

while(q != NULL){ if (q->value == elem.value){ anterior->prox = q->prox; free(q); } anterior = q; q = q->prox; }

return(result);}

9.20 printQueue()A Função printQueue recebe uma lista q e imprime na saída padrão a lista, o estado

como ela está atualmente.

void printQueue(queue *q){ while (q != NULL){ printf("%d ",q->value); q = q->prox; }}

9.21 findGodFather()A Função findGodFather recebe um hipergrafo hg, uma lista que representa os passos

de execução(passOfExecution[]) e um nodo. Ela procura o pai do nodo node que foi executado a menos tempo, retornando esse mesmo pai.

int findGodFather(hypergraph hg, int passOfExecution[], int node){ int i, j; int pai; int edge, edge2;

if(hg.BS[node].total == 0) return(-1); pai = hg.org[hg.BS[node].nodes[0]].nodes[0];

for (i=0; i<hg.BS[node].total; i++){ edge = hg.BS[node].nodes[i]; for (j=0; j<hg.org[edge].total; j++){

35

Page 36: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

edge2 = hg.org[edge].nodes[j]; if(passOfExecution[edge2] > passOfExecution[pai]) pai = edge2; } }

return(pai);}

9.22 compute()Talvez a função mais complexa do trabalho, o compute recebe um hipergrafo e a

quantidade de processadores a serem utilizados e computa o paralelismo máximo para o hipergrafo, além de calcular o número de passos utilizados com a quantidade de processadores passados como parâmetro.

int compute (hypergraph hg, int k){ int frst; int next; int qSize, queueMaxSize = 0; int steps = 0; int procAtual; int i; int j; int visited[hg.totalNodes]; int procs[k]; queue *q = NULL; queue *f = NULL; /* sets everyone as non-visited*/ for(i = 0; i < hg.totalNodes; i++) visited[i] = 0;

/*sets all processors as free*/ for(i = 0; i < k; i++) procs[i] = -1;

frst = findFirstNode(hg.BS, hg.totalNodes); q = pushQueue(q, frst); /* do it until every node is visited*/ while(!isVisited(visited, hg.totalNodes)){ if(isListVisited(hg, visited, q->value) || hg.BS[q->value].total == 0){ /* give ready nodes until there is no free proc or the list is empty */ while((next = getNextProc(procs, k)) != -1 && !isQueueEmpty(q)){ printf("Node %d to proc %d\n", q->value, next); visited[q->value] = 1; procs[next] = q->value;

q = popQueue(q); }

steps++; while((procAtual = getBusyProc(procs, k)) != -1){ /*enqueue all ready nodes*/ for(j = 0; j < hg.FS[procs[procAtual]].total; j++){ for(i = 0; i < hg.dest[hg.FS[procs[procAtual]].nodes[j]].total; i++){ if(isListVisited(hg, visited,hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i])) {

if(visited[hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]] != 1 && !isOnQueue(q, hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i])){ printf("Enqueue: "); q = pushQueue(q, hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]); printf("[%d] ", hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]); if((qSize = queueSize(q)) > queueMaxSize) queueMaxSize = qSize; } } else{ printf("##node [%d] is not ready### ", hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]); }

36

Page 37: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

} } procs[procAtual] = -1;

printf("Queue status: ["); f = q; while(q != NULL) { printf("%d ", q->value); q = q->prox; } printf("]"); q = f; //printf("Size = %d", queueSize(q)); puts(""); } } }

printf("\nMax Parallelism on this Hypergraph: %d\n", queueMaxSize); printf("Number of steps with %d proc(s): %d\n", k, steps);}

De forma a facilitar a compreensão total do algoritmo, explicaremos por partes todo o código.

/* sets everyone as non-visited*/ for(i = 0; i < hg.totalNodes; i++) visited[i] = 0;

/*sets all processors as free*/ for(i = 0; i < k; i++) procs[i] = -1;

frst = findFirstNode(hg.BS, hg.totalNodes); q = pushQueue(q, frst);

Inicializamos todos os nodos do hipergrafo como não visitados, setando sua flag de visited como 0. A seguir A Lista de processadores é varrida, e cada um dos processadores é setado como livre, assim todos poderão receber instruções. Por fim a variável frst recebe o índice do primeiro nodo livre da lista BS (Lista de hiper-arestas que incidem sobre um vértice). Então esse vértice é colocado na fila de processos prontos a serem executados.

A seguir temos:

while(!isVisited(visited, hg.totalNodes)){ Bloco }

Ou seja, enquanto tivermos nodos a serem visitados (O Algoritmo controla os nodos visitados e não visitados utilizando o vetor visited, o qual indexa os vértices com a flag de visitado, assim visited[0] mostra se o vértice 0 foi ou não visitado), o algoritmo será executado.

if(isListVisited(hg, visited, q->value) || hg.BS[q->value].total == 0){ Bloco }

A função isListVisited, definida anteriormente, retorna true se todos os vértices “pai” do vértice q->value já foram visitados. Assim, o if acima testa se o vértice atual já teve todos os seus pais visitadou ou se o vértice q->value não possui nenhuma aresta que incide nele. Assim, o bloco interno deste IF só será executado se todas as dependências do vértice já

37

Page 38: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

foram executadas ou se ele não possui dependência nenhuma.

while((next = getNextProc(procs, k)) != -1 && !isQueueEmpty(q)){ printf("Node %d to proc %d\n", q->value, next); visited[q->value] = 1; procs[next] = q->value; q = popQueue(q); } steps++;

O Laço acima será executado enquanto tivermos processadores livres e enquanto a lista de processos não estiver vazia. A variável next recebe qual é o processador livre que receberá a instrução. Feito isso setamos a flag de visitado para o nodo atual e setamos o nodo para o processador livre, então retiramos da fila de processos o processo atual e incrementamos o passo.

while((procAtual = getBusyProc(procs, k)) != -1){ Bloco }

Neste laço a variável procAtual recebe o índice do primeiro processador ocupado da lista de processadores. Caso não existam processadores ocupados, procAtual recebe -1 e o teste do laço retorna falso, saindo do mesmo.

/*enqueue all ready nodes*/for(j = 0; j < hg.FS[procs[procAtual]].total; j++){

for(i = 0; i < hg.dest[hg.FS[procs[procAtual]].nodes[j]].total; i++){if(isListVisited(hg,visited,hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i])) {

if(visited[hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]] != 1 && !isOnQueue(q, hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i])){

printf("Enqueue: ");q = pushQueue(q, hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]);printf("[%d] ", hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]);if((qSize = queueSize(q)) > queueMaxSize) queueMaxSize = qSize;

}}else{

printf("##node [%d] is not ready### ", hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]);}

}}

Temos nesse trecho de código que,para cada aresta que parte do vértice que está alocado ao processador atual (hg.FS[procs[procAtual]]), testamos todos os vértices que são alcançáveis a partir dele. Estes vértices que têm o processo (ou vértice do hipergrafo) procs[procAtual] como pai, só poderão ser colocados na fila de execução, quando todas as suas dependências ( ou processos pai ) forem processadas, ou seja, quando todos os processos pai estiverem com a flag ( indexada no vetor visited[]) setada. Esse teste é feito pelo seguinte if:

if(isListVisited(hg,visited,hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i])) { Bloco }else{

printf("##node [%d] is not ready### ", hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]);}

Quando o vértice testado atende a todos os testes definidos acima, ele ainda passa por uma última validação:

if(visited[hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]] != 1 && !isOnQueue(q, hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i])){ Bloco }

38

Page 39: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Nessa validação testamos se o vértice a ser enfileirado já existe dentro da fila de processos prontos, ou seja, se a função isOnQueue retornar false saberemos que o vértice ainda não foi enfileirado, assim estará pronto para entrar para a fila.

A seguir, enfileiramos o processo, como pode ser visto no trecho de código abaixo:

printf("Enqueue: ");q = pushQueue(q, hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]);printf("[%d] ", hg.dest[hg.FS[procs[procAtual]].nodes[j]].nodes[i]);if((qSize = queueSize(q)) > queueMaxSize) queueMaxSize = qSize;

A última parte da função desaloca o processador atual como processador em uso ( ou seja, torna o processador atual livre para receber novos processos) e imprime o status da fila de status.

procs[procAtual] = -1; printf("Queue status: ["); f = q; while(q != NULL) { printf("%d ", q->value); q = q->prox; } printf("]"); q = f; //printf("Size = %d", queueSize(q)); puts("");

9.23 bestSolution()

queue **bestSolution(hypergraph hg, int k){int steps = 0;//numero de passos da execucaoint node, edge;int visited[hg.totalNodes];//guarda se o nó já foi executadoint i,j,l;int procs[k];queue *q1 = NULL;queue *q2 = NULL;queue *qa = NULL;queue *qb = NULL;queue *qc = NULL;hyperstruct *result;

/* sets everyone as non-visited*/for(i = 0; i < hg.totalNodes; i++) visited[i] = 0;

/*sets all processors as free*/for(i = 0; i < k; i++) procs[i] = -1;

node = findFirstNode(hg.BS, hg.totalNodes);q1 = pushQueue(q1, node);

while(!isQueueEmpty(q1)){qa = q1;

while (qa!=NULL){for (i=0; i<hg.FS[qa->value].total; i++){

edge = hg.FS[qa->value].nodes[i];for (j=0; j<hg.dest[edge].total; j++){

if (isOnlyDependency(hg, hg.dest[edge].nodes[j], visited, q1) && !isOnQueue(q2, hg.dest[edge].nodes[j])){

q2 = pushQueue(q2, hg.dest[edge].nodes[j]);}

}}qa = qa->prox;

}

if (q2 != NULL){

39

Page 40: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

qa = getNodesNotDependents(hg, q1, q2);}

printf("\n#### PASSO %d ####",steps);printf("\nFilas antes da otimização--");printf("\nq1:\t");printQueue(q1);printf("\nq2:\t");printQueue(q2);

printf("\n-- Movendo os seguintes vértices de q1 para q2: ");while (queueSize(q1) > queueSize(q2) && qa != NULL){

printf("%d ",qa->value);q2 = pushQueue(q2,qa->value);q1 = removeElementFromQueue(q1, *qa);qa = popQueue(qa);

}

printf("\n-- Filas após a otimização--");printf("\nq1:\t");printQueue(q1);printf("\nq2:\t");printQueue(q2);

if (k < 0){printf("\nExecutando vértices em q1: ");while (q1 != NULL || l < k){

visited[q1->value] = 1;printf("%d ",q1->value);q1 = popQueue(q1);l++;

}printf("\n");q1 = q2;q2 = NULL;

}else{for (i=0; q1 != NULL && i < k; i++){

printf("\nNó %d para processador %d",q1->value,i);visited[q1->value] = 1;q1 = popQueue(q1);

}qa = q1;while (qa != NULL){

qb = q2;while (qb != NULL){

if (isDependent(hg, qb->value, qa->value)){qc = qb->prox;printf("\n*** Retirando nó %d de q2 (depende de %d)",qb->value,qa-

>value);q2 = removeElementFromQueue(q2, *qb);qb = qc;

}else{qb = qb->prox;

}

}qa = qa->prox;

}qa = q1;if (qa != NULL){

while (qa->prox != NULL){qa = qa->prox;

}qa->prox = q2;

}else{q1 = q2;

}

q2 = NULL;}

steps++;}printf("\n");

}

40

Page 41: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Assim como fizemos com o algoritmo compute, vamos explicar o algoritmo bestSolution por partes, de modo a tornar a compreenção do mesmo mais simples.

for(i = 0; i < hg.totalNodes; i++) visited[i] = 0;

for(i = 0; i < k; i++) procs[i] = -1;

Neste trecho setamos todos os nodos do hipergrafo como não visitados e os processadores como livres para receberem processos.

node = findFirstNode(hg.BS, hg.totalNodes);q1 = pushQueue(q1, node);

Buscamos o primeiro nodo e o colocamos na lista de execução q1.

(1) while(!isQueueEmpty(q1)){ Bloco }

Este laço é responsável por testar se a fila de execução foi totalmente executada, ou seja, se a fila está vazia. Assim, enquanto tivermos processos a serem executados na fila, o Bloco será executado.

qa = q1;while (qa!=NULL){

for (i=0; i<hg.FS[qa->value].total; i++){edge = hg.FS[qa->value].nodes[i];for (j=0; j<hg.dest[edge].total; j++){

if (isOnlyDependency(hg, hg.dest[edge].nodes[j], visited, q1) && !isOnQueue(q2, hg.dest[edge].nodes[j])){

q2 = pushQueue(q2, hg.dest[edge].nodes[j]);}

}}qa = qa->prox;

}

No laço acima, o algoritmo navega pela lista qa. Para cada um dos nodos pertencentes à fila, visita todos os nodos dependentes do nodo atual (qa->value) e verifica se as dependências dos mesmos já foram executadas. Caso um nodo que seja filho de qa->value tenha toda as suas dependências executadas, ele é colocado na fila de execução q2.

if (q2 != NULL){qa = getNodesNotDependents(hg, q1, q2);

}

printf("\n#### PASSO %d ####",steps);printf("\nFilas antes da otimização--");printf("\nq1:\t");printQueue(q1);printf("\nq2:\t");printQueue(q2);

printf("\n-- Movendo os seguintes vértices de q1 para q2: ");while (queueSize(q1) > queueSize(q2) && qa != NULL){

printf("%d ",qa->value);q2 = pushQueue(q2,qa->value);q1 = removeElementFromQueue(q1, *qa);qa = popQueue(qa);

}

41

Page 42: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

printf("\n-- Filas após a otimização--");printf("\nq1:\t");printQueue(q1);printf("\nq2:\t");printQueue(q2);

Caso a lista de execução criada no laço anterior não seja vazia, inserimos em qa os nodos que não possuem dependências.

A seguir imprimimos na saída padrão o status do passo atual, mostrando como as filas de execução estão compostas.

Feito isso, o algoritmo realiza a estruturação das filas q1 e q2, movendo vértices de uma fila para outra, até que o tamanho da fila q1 seja, no máximo, igual ao tamanho da fila q2.

if (k < 0){printf("\nExecutando vértices em q1: ");while (q1 != NULL || l < k){

visited[q1->value] = 1;printf("%d ",q1->value);q1 = popQueue(q1);l++;

}printf("\n");

q1 = q2;q2 = NULL;

}

Caso a quantidade de processadores tenha sido definida no começo da exeução do algoritmo (quando k < 0 ) ...

else{for (i=0; q1 != NULL && i < k; i++){

printf("\nNó %d para processador %d",q1->value,i);visited[q1->value] = 1;q1 = popQueue(q1);

}qa = q1;while (qa != NULL){

qb = q2;while (qb != NULL){

if (isDependent(hg, qb->value, qa->value)){qc = qb->prox;printf("\n*** Retirando nó %d de q2 (depende de %d)",qb->value,qa->value);q2 = removeElementFromQueue(q2, *qb);qb = qc;

}else{qb = qb->prox;

}

}qa = qa->prox;

}qa = q1;if (qa != NULL){

while (qa->prox != NULL){qa = qa->prox;

}qa->prox = q2;

}else{q1 = q2;

}q2 = NULL;

}

42

Page 43: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

Caso tenha sido definida uma quantidade válida de processadores (k > 0), setamos a todos os processadores um processo existente em q1, até que todos os processadores tenham recebido processos ou até que a fila de execução q1 tenha ficado vazia. Carregamos então q1 para a fila qa, e enquanto essa fila qa não for vazia, buscamos dentro de qb (fila de execução definida por q2) vértices que possuem dependência com os vértices pertencentes a qa. Quando um vértice de qb é dependente de um vértice de qa, este vértice é removido de q2.

Ao fim do laço de repetição, qa recebe a fila q1. Caso qa não seja nula, ao final da fila qa é inserida a fila q2, ou seja, qa = [q1][q2], sendo q1 a cabeça e q2 a cauda da fila. Caso contrário, a fila q1 recebe a fila q2.

Steps++;

Por fim o passo atual é incrementado e o loop (1) é executado.

9.24 readHList()A Função readHList lê o arquivo texto contendo a definição do hipergrafo que foi passado por parâmetro para o programa e instancia esse hipergrafo nas estruturas hyperstruct *listOrg e hyperstruct *listDest.void readHList(hyperstruct *listOrg, hyperstruct *listDest, int haTotal){ int i, j, k, l; char aux1[LINE]; char aux2[LINE]; char value[LINE];

for(i = 0; i < haTotal; i++){ fgets(aux1, LINE, stdin); fgets(aux2, LINE, stdin); j = 0; while(aux1[j] != '\n'){ l = 0; while(aux1[j] != ' ' && aux1[j] != '\n'){ value[l++] = aux1[j++]; } value[l] = '\0'; listOrg[i] = createNode(listOrg[i], atoi(value)); if(aux1[j] != '\n') j++; } k = 0; while(aux2[k] != '\n'){ l = 0; while(aux2[k] != ' ' && aux2[k] != '\n'){ value[l++] = aux2[k++]; } value[l] = '\0'; listDest[i] = createNode(listDest[i], atoi(value)); if(aux2[k] != '\n') k++; } }}

9.25 printNodes()A Função printNodes imprime na saída padrão todos os nodos pertencentes a estrutura

hiperstruct ha passada como parâmetro.

void printNodes(hyperstruct ha){ int i; for(i = 0; i < ha.total; i++){ printf("%d ", ha.nodes[i]); } puts("");}

43

Page 44: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

9.26 fullPipelining()Aqui implementamos o algoritmo que busca a otimização na execução de instruções

considerando somente um processador pipeline. A implementação segue os mesmos passos descritos na exposição feita na seção 5.4.3. Expomos abaixo o código fonte da implementação feita em c.

instruct *fullPipelining(hypergraph hg) {

int i, j, edge; int cont = 0; queue *q = NULL; /*guarda os nós liberados mas não executados*/ int executed[hg.totalNodes];//guarda se o nó já foi executado int passOfExecution[hg.totalNodes];//guarda o passo em que o nó foi executado int execution[hg.totalNodes];//guarda qual nó foi executado no passo i int dist[hg.totalNodes]; //armazena a distância de um hiper-nodo i para seu paiint father; int fathers[hg.totalNodes]; //armazena os pais dos hiper-nodos

/* pegando os primeiros nós */ for(i = 0; i < hg.totalNodes; i++){

dist[i] = 0; executed[i] = 0; if (hg.BS[i].total == 0){

q = pushQueue(q, i); }

} int aux, x; instruct instA;

while(!isVisited(executed, hg.totalNodes)){ for (i=0; i<hg.FS[q->value].total; i++){

edge = hg.FS[q->value].nodes[i]; for (j=0; j<hg.dest[edge].total; j++){

if (isOnlyDependency(hg, hg.dest[edge].nodes[j], executed, q) && !isOnQueue(q, hg.dest[edge].nodes[j])){

q = pushQueue(q, hg.dest[edge].nodes[j]); }

} }

executed[q->value] = 1; passOfExecution[q->value] = cont; execution[cont] = q->value;

if((father = findGodFather(hg, passOfExecution, q->value)) == -1){ fathers[q->value] = -1; dist[q->value] = 0; cont++; q = popQueue(q); continue;

} fathers[q->value] = father; dist[q->value] = cont - passOfExecution[father];

//INICIO DA OTIMIZACAO

44

Page 45: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

//Calcula a nova posicao do pai aux = cont - (dist[father] + dist[q->value])/2; //se não utrapassar o filho otimiza if (aux < passOfExecution[father]){

//shift no vetor execution a partir da posicao cont printf("\n**Movendo %d de %d para %d: %d < %d", father, passOfExecution[father],

aux, aux, cont); for (x = passOfExecution[father]; x >= aux; x--){

execution[x] = execution[x-1]; }

execution[aux] = father;

//Atualizando o passOfExecution for(x=0; x<=cont; x++){

passOfExecution[execution[x]] = x; }

//Atualizando distancias for(x=0; x<=cont; x++){

//dist[q->value] = cont - passOfExecution[father]; if (fathers[execution[x]] >= 0){

dist[execution[x]] = x - passOfExecution[fathers[execution[x]]]; }else{

dist[execution[x]] = 0; }

} } //FIM DA OTIMIZAÇÃO

cont++; q = popQueue(q);

}

printf("\n");

aux = 0; /* imprimindo nós */ for(i = 0; i < hg.totalNodes; i++){

printf("passo %d: %d com pai %d (%d)\n",i, execution[i], fathers[execution[i]], dist[execution[i]]);

aux += dist[execution[i]]; }

printf("Distancia média %d\n",aux/(cont+1));

} No primeiro loop, inicializamos os vetores de distâncias, de controle dos vértices

executados e enfileiramos os nós que não apresentem dependências para iniciarmos a execução:

for(i = 0; i < hg.totalNodes; i++){ dist[i] = 0; executed[i] = 0; if (hg.BS[i].total == 0){

q = pushQueue(q, i); }

}

45

Page 46: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

O controle da execução de todos os vértices é feito pelo while principal do algoritmo. Ele invoca a função isVisited que retorna verdadeiro se todos os vértices já foram executados, garantindo que mesmo que o hipergrafos não seja conexo, este seja completamente executado.

while(!isVisited(executed, hg.totalNodes))

No for analisamos todos os filhos do primeiro vértice da fila, verificando quais são liberados com a execução desse.

for (i=0; i<hg.FS[q->value].total; i++){ edge = hg.FS[q->value].nodes[i]; for (j=0; j<hg.dest[edge].total; j++){ if (isOnlyDependency(hg, hg.dest[edge].nodes[j], executed, q) && !isOnQueue(q, hg.dest[edge].nodes[j])) { q = pushQueue(q, hg.dest[edge].nodes[j]); } } }

Nos passos seguintes, setamos o hiper-nodo como executado, gravamos o passo que foi executado e guardamos o hiper-nodo que foi executado nesse passo, respectivamente.

executed[q->value] = 1; passOfExecution[q->value] = cont; execution[cont] = q->value;

Seguindo o algoritmo, o próximo passo é verificar se o hiper-nodo a ser processado possui um pai. Se esse pai não existir significa que esse hiper-nodo não apresenta dependência no hipergrafo assim, por default, sua distância será 0 e seu pai será igual a -1 e o próximo passo deverá ser iniciado.

if((father = findGodFather(hg, passOfExecution, q->value)) == -1){ fathers[q->value] = -1; dist[q->value] = 0; cont++; q = popQueue(q); continue;

}

Caso a condição do if anterior falhe, armazenaremos o pai desse hiper-nodo e calcularemos a distância entre pai e filho.

fathers[q->value] = father; dist[q->value] = cont - passOfExecution[father];

Atualizadas as estruturas com essa execução, iniciaremos o processo de otimização primeiramente calculando a posição intermediária entre pai e filho.

aux = cont - (dist[father] + dist[q->value])/2;

Se a posição calculada não ultrapassar o filho dá-se início a otimização

46

Page 47: UNIVERSIDADE FEDERAL DO PARANÁ - inf.ufpr.br · vazio e finito V de vértices e um conjunto finito A de arcos, onde “a” ϵ A é um par ordenado de elementos de V, ou seja, “a”

if (aux < passOfExecution[father]){

Primeiro realizamos um shift no vetor execution “abrindo” lugar para a nova alocação do pai do hiper-nodo executado.

for (x = passOfExecution[father]; x >= aux; x--){ execution[x] = execution[x-1];

}

Alocamos o pai para a posição calculada

execution[aux] = father;

Finalizando a execução, atualizamos todas as listas de armazenamento

for(x=0; x<=cont; x++){ if (fathers[execution[x]] >= 0){

dist[execution[x]] = x - passOfExecution[fathers[execution[x]]]; }else{

dist[execution[x]] = 0; }

}

Os próximos passos somente imprimem na saída o resultado obtido pelo algoritmo.

47