diff --git a/doc/relatorio/relatorio_t2.tex b/doc/relatorio/relatorio_t2.tex index 3c84dcc81de6f96bf6df9c0aa05be6db3f6cba8e..be22361d68059be14128bd4137c6b2495db27c65 100644 --- a/doc/relatorio/relatorio_t2.tex +++ b/doc/relatorio/relatorio_t2.tex @@ -38,15 +38,16 @@ \begin{abstract} O algoritmo de Floyd Warshall consiste em encontrar todos os menores caminhos - entre pares de vértices de um grafo. - Esse trabalho se propõe a implementar esse algoritmo de forma serial e paralela + entre pares de vértices de um grafo com peso, direcionado ou não. Uma de suas + restrições é que não pode haver ciclos negativos no grafo. + Esse trabalho se propõe a implementar esse algoritmo de forma sequencial e paralela considerando uma matriz de adjacência usando apenas uma dimensão (row-wise). \end{abstract} \section{Desenvolvimento} - O algoritmo foi escrito em linguagem C++ e consiste na utilização + O algoritmo foi implementado em linguagem C++ e consiste na utilização de uma matriz de adjacência com tamanho k x k. Inicialmente, o grafo é lido do arquivo de entrada na matriz de @@ -61,7 +62,7 @@ \subsection{Algoritmo sequencial} - O algoritmo sequencial é bem simples, consistindo de três laços, + O algoritmo sequencial é bem simples, consistindo de três laços, o primeiro para as k-etapas e os outros dois para a varredura de todas as arestas da matriz de adjacência. @@ -241,10 +242,10 @@ index % time self children called name \subsection{Tempo de Execução} \subsubsection{Compilação com e sem flags de otimização} - + Utilizando as flags de otimização (-O3 -funroll-loops -ftree-vectorize -march=native), acontece uma queda considerável no tempo de execução do programa. - + \includegraphics[scale=0.35]{imagens/compila_com_sem_flags} \includegraphics[scale=0.585]{imagens/compila_com_flags} @@ -273,10 +274,10 @@ index % time self children called name \subsection{Lei de Amdahl} - A lei de Amdahl parte do tempo de execução sequencial para estimar o + A lei de Amdahl parte do tempo de execução sequencial para estimar o Speedup máximo utilizando múltiplos processadores. - Levando-se em conta que o tempo exclusivamente sequencial é de 48.83\% e o + Levando-se em conta que o tempo exclusivamente sequencial é de 48.83\% e o restante (51.17\%) é o que se acredita ser paralelizável, tem-se os seguintes resultados: \begin{items} @@ -312,7 +313,7 @@ index % time self children called name A lei de Gustafson-Barsis parte do tempo de execução em paralelo para estimar o Speedup máximo comparado com a execuçao sequencial. - Levando-se em conta que o tempo exclusivamente sequencial é de 48.83\% e o + Levando-se em conta que o tempo exclusivamente sequencial é de 48.83\% e o restante (51.17\%) é o que se acredita ser paralelizável, tem-se os seguintes resultados: \begin{items} @@ -348,7 +349,7 @@ index % time self children called name \begin{center} \includegraphics[scale=0.45]{imagens/eficiencia} \end{center} - + \subsection{Speedup} Levando-se em conta que temos um caso de escalabilidade forte por @@ -365,10 +366,79 @@ index % time self children called name \includegraphics[scale=0.45]{imagens/speedup} \end{center} + \section{Complexidade} + + \subsection{Sequencial} + Na versão sequencial, tem-se claramente a seguinte complexidade: + \begin{items} + \item \texttt{Busca na matriz de adjâcencia}: \newline + Como deve-se passar n-etapas, percorrendo toda a matriz, a + complexidade é de $ O(|V|^3) $ no pior ou melhor caso; \newline + \item \texttt{Inicialização da matriz de adjacência}: \newline + Como é necessário inicializar toda a matriz de adjacência, temos + temos aqui $ O(|V|^2) $. + \end{items} + + \subsection{Paralela} + Desconsiderando o tempo de leitura do arquivo de entrada e + criação da matriz de adjacência (que será igual na implementação + paralela), temos dois trechos de código candidatos à implementação + em paralelo: + \begin{items} + \item \texttt{Busca na matriz de adjâcencia}; \newline + \item \texttt{Inicialização da matriz de adjacência}. + \end{items} + + \section{Implementação paralela} + + \subsection{Modelo PRAM} + Na seção Complexidade estão as duas partes do algoritmo que + podem ser melhoradas se implementadas em paralelo. + Uma das vantagens no modelo PRAM é o fato de se poder usar + tantos processadores quanto forem necessários. + Com isso em mente, podemos melhorar as três partes do código + da seguinte maneira: + \begin{items} + \item \texttt{Busca na matriz de adjâcencia}: \newline + Como deve-se passar n-etapas, percorrendo toda a matriz, a + complexidade é de $ O(|V|) $ no pior ou melhor caso, + utilizando-se $ p ^ 2 $ processadores, levando-se em conta + que o laço das k-etapas não é facilmente paralelizável; \newline + \item \texttt{Inicialização da matriz de adjacência}: \newline + Como é necessário passar por todos os elementos dos vetores + de vértices anteriores e distância, temos aqui $ O(1) $. + \end{items} + O trabalho executado em paralelo é o mesmo que na versão sequencial, + já que não existem operações adicionais de gerenciamento e todas + as arestas da matriz de adjacência devem ser visitadas. \newline + Dessa forma, teremos um ganho considerável no speedup comparando as + duas versões. + \begin{items} + \item \texttt{$n^2$ processadores}: \newline + \begin{equation} + S_p(n^2) = \frac{|V|^3}{|V|} = |V| ^ 2 + \end{equation} + \end{items} + + Temos aqui um speedup linear, na teoria. + Na prática, o ideal é limitarmos o número de threads para o mesmo + da arquitetura, pois se tivermos $n^2$ threads, apenas algumas estarão + em execução ao mesmo tempo, enquanto as outras esperarão até que + aconteça uma troca de contexto, gerando um overhead muito grande. + + \newpage + \section{Análise do Código e resultados obtidos} + O código foi implementado e testado no seguinte hardware: + \begin{items} + \item Intel® Core™ i5-4210U CPU @ 1.70GHz x 4 (2 threads / 4 núcleos); + \item 6GB de RAM; + \item Sistema Operacional Linux Debian SID. + \end{items} + \subsection{Fontes de ganho e queda de desempenho} - Existem algumas fontes influenciadoras de desempenho. + Existem algumas fontes influenciadoras, que podem melhorar ou piorar o desempenho. Ganhos: \begin{items} @@ -384,6 +454,11 @@ index % time self children called name diminuição na granularidade, no último laço. Cada thread passa a ser responsável apenas por calcular uma aresta da matriz de adjacência. O tempo chega a dobrar nesse caso; \newline + \item \texttt{schedule}: alterar o schedule de static para dynamic ou guided + piora um pouco o desempenho. Tendo em vista que as tarefas são + homogêneas e cada thread vai executar uma quantidade parecida, pode-se + escalonar inicialmente todas as tarefas para cada uma delas. \newline + Alterar o valor do chunk para o static schedule também não melhora nada. \end{items} \subsection{Resultados obtidos} @@ -403,63 +478,6 @@ index % time self children called name diminuição do tempo (baseando-se na parte do código que pode ser paralelizado), quanto na tendência do overhead, speedup e eficiência. - \section{Complexidade} - - \subsection{Sequencial} - Desconsiderando o tempo de leitura do arquivo de entrada e - criação da lista de adjacência (que será igual na implementação - paralela), temos três trechos de código candidatos à implementação - em paralelo: - \begin{items} - \item \texttt{Busca na árvore binária}: \newline - Utilizando a estrutura \emph{set} do C++, que utiliza uma árvore binária, - temos como complexidade $ O(|A| * log |V|) $; \newline - \item \texttt{Desempilha / Empilha}: \newline - O empilhamento/desempilhamento, possui custo $ O(log |V|) $; \newline - \item \texttt{Inicialização das estruturas de dados (vetor de vértices e caminho)}: \newline - Como é necessário passar por todos os elementos dos vetores - de vértices anteriores e distância, temos aqui $ O(|V|) $. - - Considerando um grafo conexo, temos: $ |A| \leq |V| - 1 $ ou - $ |V| \geq |A| + 1 $. - Assim, a complexidade total é igual à - $ O(|V| * log |V| + |A| * log |V|) $, ou $ (|A| * log |V|) $. - \end{items} - - \section{Implementação paralela} - - \subsection{Modelo PRAM} - Na seção Complexidade estão as três partes do algoritmo que - podem ser melhoradas se implementadas em paralelo. - Uma das vantagens no modelo PRAM é o fato de se poder usar - tantos processadores quanto forem necessários. - Com isso em mente, podemos melhorar as três partes do código - da seguinte maneira: - \begin{items} - \item \texttt{Busca na árvore binária}: \newline - Possuindo $n^2$ processadores, podemos utilizar um - algoritmo de busca com complexidade O(1), visto em - sala de aula. - \item \texttt{Desempilha / Empilha}: O(log V); \newline - A princípio, fica igual. - \item \texttt{Inicialização das estruturas de dados (vetor de vértices e caminho)}: \newline - Com $2n$ processadores, cada um ajustando o valor - inicial em algum item do vetor de anteriores ou do - vetor de distâncias, teremos aqui também complexidade - O(1). - \end{items} - - A diferença nessa abordagem, fica por conta da separação entre - p-processadores. - - Inicialmente, o vetor é separado em p clusters, de modo - que os menores caminhos sejam calculados para cada cluster, - baseado no vértice que estiver mais perto do vértice inicial. - - Após o cálculo local, a distância será enviada a todos os - processadores, para que atualizem os seus respectivos vetores - de distância. - \newpage \section{Execução do código} @@ -475,7 +493,7 @@ index % time self children called name \end{lstlisting} \item \texttt{Compile e gere a documentação}: \begin{lstlisting} - $ make dijkstra && make doc + $ make && make doc \end{lstlisting} \item \texttt{Execute} \begin{lstlisting} @@ -497,9 +515,9 @@ index % time self children called name \end{lstlisting} \end{items} - \section{Verificação de corretude} + \section{Utilitários} - \subsection{Script de benchmark} + \subsection{Teste de corretude - script de benchmark} No diretório /build/ do projeto foi criado um script (gera\_bench.sh) que roda todos os executáveis com todas as entradas de grafos cíclicos (do diretório /entrada/) e joga a saída (matriz de adjacência final, depois de todas as atualizações feitas) @@ -507,4 +525,15 @@ index % time self children called name As saídas foram checadas e são iguais para as mesmas entradas. + \subsection{Gerador de entradas} + No diretório /entrada/ do projeto foi criado um script (gera\_grafo.py) que + gera entradas cíclicas ou aciclicas. + \begin{items} + \item \texttt{Para gerar a entrada, basta executar o script}: + \begin{lstlisting}[language=bash] + $ ./gera_grafo A 50 + $ ./gera_grafo C 50 2 + \end{lstlisting} + \end{items} + \end{document}