• Nenhum resultado encontrado

Propósito Geral

CAPÍTULO 4 Algoritmos em Grafos

4.3.2 Algoritmo Sequencial de Dijkstra

O algoritmo de Dijkstra [12] fornece uma solução eficiente para o problema do caminho mínimo tanto para o SSSP quanto para o APSP, embora, para solucionar este, tenha que ser aplicado para cada vértice do grafo. O algoritmo trabalha sobre um grafo no qual todos os pesos das arestas são positivos. Assim, constrói um caminho mínimo a partir de um único vértice origem s para todos outros vértices, utilizando uma técnica conhecida como relaxamento de arestas.

O processo de relaxamento de uma aresta (u, v) consiste em testar se é possível melhorar o menor caminho s encontrado até o momento passando por u. Se for possível, são atualizados a distância e o predecessor de v. O relaxamento pode simplesmente reduzir o valor do menor caminho. Quando o relaxamento de uma aresta (u, v) ocorre, é dito que o vértice v foi alcançado.

O Algoritmo 4.7 representa a solução sequencial de Dijsktra. As distâncias da origem para cada vértice v são mantidas em d[v] e os predecessores estão contidos em p[v]. Inicialmente, cada distância d[v] possui valor infinito, exceto a do vértice de origem que contém o valor zero. Um conjunto de vértices S é mantido, onde v ∈ S e d[v] é o valor da menor distância da origem até o vértice v. O algoritmo seleciona repetidamente um vértice u ∈ V − S com o menor caminho estimado, adiciona-o a S e, somente então, relaxa todas as arestas de u. Uma fila de prioridades é utilizada para selecionar o vértice com menor caminho [13] [3] [22].

4.3 Caminho Mínimo entre Todos os Pares de Vértices 64

Algoritmo 4.7: Algoritmo Sequencial de Dijsktra

Entrada: Grafo G = (V, E) e um vértice origem s; Saída: Lista de predecessores p e o caminho mínimo d;

1 para cada vértice v ∈ V faça 2 d[v] ← ∞ 3 p[v] ← null 4 fim 5 d[s] ← 0 6 S← /0 7 Q← V

8 enquanto não EMPTY (Q) faça 9 u← EXT RACT − MIN(Q) 10 S= S ∪ {u}

11 para cada vértice v em ad j[u] faça 12 se v /∈ S e d[v] > d[u] + w(u, v) então

13 p[v] ← u 14 d[v] ← d[v] + w(u, v) 15 DECREASE_KEY (Q, v, w) 16 fim 17 fim 18 fim

O relaxamento das arestas ocorre nas linhas 12 a 16 no Algoritmo 4.7. A cada iteração do laço da linha 8, um vértice é obtido pela chamada ao método EXTRACT-MIN. Esse método extrai o elemento com menor valor na fila de prioridade. O algoritmo finaliza quando S = V , ou seja, Q = /0. Uma característica importante do algoritmo é a invariância do laço da linha 8, que tem o objetivo de manter o conteúdo da fila de prioridade, onde Q= V − S. Isso faz com que o algoritmo escolha o vértice mais próximo em V − S para adicionar ao conjunto S. Assim, confirma-se a presença da estratégia gulosa no algoritmo de Dijkstra [3].

A complexidade de tempo depende da estrutura de dados implementada na fila de prioridade. Pois, as operações EXTRACT-MIN, DECREASE-KEY e INSERT (implícito na linha 7) pertencentes à fila de prioridade são executadas para cada vértice. Com a estrutura heap, o algoritmo executa em tempo de O(|E|log|V |). Se o heap de Fibonacci é usado, a complexidade do algoritmo passa a ser O(|E| + |V |log|V |). Caso seja utilizada a estrutura lista, a complexidade aumenta para O(V2) [13] [3] [22] [29].

Semelhante ao Algoritmo 4.3, o algoritmo de Dijkstra, para solucionar o pro- blema APSP, requer sua invocação repetida para cada vértice v ∈ V . O Algoritmo 4.8

4.3 Caminho Mínimo entre Todos os Pares de Vértices 65

descreve o algoritmo Dijkstra para o problema APSP. A complexidade de tempo do Al- goritmo 4.8 para o melhor caso citado anteriormente é O(|V ||E|log|V |) e, para o pior caso, O(|V |3). Portanto, embora seja eficiente, o algoritmo de Dijkstra requer um grande esforço computacional para solucionar o problema APSP.

Algoritmo 4.8: Algoritmo Sequencial de Dijkstra para o Problema APSP

Entrada: Grafo G(V, E);

Saída: União de listas de predecessores p e o caminho mínimo d;

1 para cada vértice v ∈ V faça 2 Dijkstra Sequencial(G, v) 3 fim

4.3.3

Outras Abordagens

A solução sequencial proposta por Venkataraman et al. [39] faz com que o algoritmo Floyd-Warshall utilize a memória cache da CPU de forma eficiente. O método particiona a matriz de distâncias em ladrilhos. A Figura 4.7 ilustra uma matriz dividida em 4×4 ladrilhos. Dentro de cada ladrilho, múltiplas iterações são executadas. Os ladrilhos são executados em uma certa ordem, onde o ladrilho primário, situado ao eixo diagonal da iteração correspondente, define a ordem da execução dos ladrilhos. Isso facilita, de certa forma, a localidade na memória cache, diminuindo valores constantes do tempo computacional. Entretanto, ele não altera a complexidade do algoritmo Floyd-Warshall. O ganho de speedup situa-se entre 1,6x e 2x [39]. A Figura 4.8 representa, de forma geral, a ordem de execução dos ladrilhos de uma matriz de distâncias em cada iteração. O primeiro ladrilho calculado é o de cor vermelha (cinza escuro) e, em seguida, os ladrilhos cinzas claro e, por último, os ladrilhos brancos.

Figura 4.7: Divisão de uma matriz de distâncias em ladrilhos.

Já à abordagem paralela utilizada por Harish e Narayanan [16] divide o algoritmo Floyd-Warshall em duas partes. Uma parte do algoritmo é executada na CPU, onde uma função kernel é chamada várias vezes, como mostrado no Algoritmo 4.9. A segunda parte

4.3 Caminho Mínimo entre Todos os Pares de Vértices 66

Figura 4.8: Solução sequencial proposta por Venkataraman et al. [39].

é executada na GPU, onde uma única função kernel é executada por O(|V2|) threads. Cada thread é mapeada para um único elemento da matriz, consequentemente, cada bloco de threads trabalha sobre uma única parte da matriz. A Figura 4.9 ilustra esse mapeamento de threads sobre a matriz de distância feito pelo algoritmo de Harish e Narayanan [16]. A função kernel é chamada O(|V |) vezes sequencialmente pela CPU, logo, várias comunicações são realizadas. Contudo, uma única transferência de dados significativa é feita, pois os dados são mantidos na memória da GPU até o final do processamento. De acordo com Xiao e Feng [41], uma grande parte do tempo de execução de uma aplicação CUDA é gasto no lançamento do kernel e na sincronização. Isso ocorre quando há um uso intensivo de comunicação entre CPU e GPU. Entretanto, apesar desta limitação, essa solução oferece um ganho de speedup sobre a implementação sequencial [16].

Figura 4.9: Solução paralela do algoritmo Floyd-Warshall pro- posta por Harish e Narayanan [16].

4.3 Caminho Mínimo entre Todos os Pares de Vértices 67

Algoritmo 4.9: Código CPU - Implementação Paralela do Algoritmo do Floyd-Warshall proposta por Harish et al. [16].

Entrada: Grafo G representado pela matriz de distâncias Anxn.;

Saída: Caminho mínimo do grafo G representado pela matriz de distâncias Anxn;

1 bloco_size ← número máximo de threads por bloco 2 Aloca memória para a matriz Anxnna GPU

3 Copia a matriz Anxn para GPU

4 tam_bloco ← bloco_size × bloco_size 5 tam_grade ← d(blocon_size)e × d(blocon_size)e 6 para estagio = 0 até n − 1 faça

7 Invoca o kernel <tam_grade, tam_bloco >(Anxn, estagio) na GPU 8 fim

9 Copia a matriz Anxn da GPU para CPU

10 Libera o espaço na memória da GPU ocupado pela matriz Anxn

O algoritmo paralelo proposto por Katz e Kider [20] é baseado no algoritmo sequencial implementado por Venkataraman et al. [39]. O algoritmo particiona a matriz de distâncias em blocos de tamanhos iguais. Cada bloco fica com 16×16 vértices de tamanho. Dessa forma, o bloco pode ser facilmente mapeado para os blocos de threads. O algoritmo procede em estágios, cada um constituído por três fases. As fases são executadas por kernels. Em todos os estágios, um bloco primário é definido. Esse bloco primário fica situado ao longo do eixo diagonal da matriz de distâncias, que se inicializa na localização (0, 0), e o último bloco fica em (|V|-16, |V|-16), onde V é o conjunto de vértices. Assim, o algoritmo consiste em |V |16 iterações.

A Fase 1 é a mais simples. Somente o bloco primário atual executa o algoritmo, e um único SM da GPU permanece ativo. O bloco é obtido somente uma vez na memória global e levado para a memória compartilhada, de modo a acelerar a fase tanto quanto possível.

A Fase 2 manipula os blocos cujos valores são dependentes do bloco primário. Esses blocos estão localizados na mesma linha (eixo x) e coluna (eixo y) do bloco primário computado. Tais blocos são chamados blocos simplesmente-dependentes [20]. Para a realização dessa fase, o bloco primário da matriz de distâncias é carregado para a memória compartilhada. Por exemplo, para uma matriz com n blocos por eixo, 2×(n - 1) blocos da matriz são computados e, consequentemente, o mesmo número de blocos de threads são organizados no grid.

A Fase 3 computa os blocos restantes da matriz, conhecidos por blocos duplamente-dependentes, por dependerem da computação da linha e coluna do bloco pri-

4.3 Caminho Mínimo entre Todos os Pares de Vértices 68

mário. A Figura 4.10 fornece uma visualização de alto nível da execução das três fases. Vale ressaltar que cada fase é executada sequencialmente, resultando numa comunicação constante entre a CPU e a GPU.

Figura 4.10: Visão geral do algoritmo proposto por Katz e Ki- der [20].

CAPÍTULO

5

Documentos relacionados