Depois de estudar este capítulo, o leitor deverá ser sabedor da importância da concorrência, da escalabilidade, da localidade e da modularidade no desenho de programas paralelos. Deverá, também, estar familiarizado com o modelo idealizado do multi-computador para o qual iremos desenhar os algoritmos paralelos e as abstracções de computação e de comunicação que iremos usar quando descrevermos os algoritmos paralelos.
O paralelismo tem muitas vezes sido visto como uma estranha e esotérica sub-área da computação, interessante mas de pequena relevância para o programador médio. O estudo das tendências das aplicações, das arquitecturas de computadores, e das redes mostra que esta perspectiva não é mais sustentável. O paralelismo está a tornar-se ubíquo e a programação paralela está a tornar-se central no empreendimento da programação.
Tradicionalmente, o desenvolvimento de aplicações de elevado desempenho tem vindo a ser motivado pela simulação de sistemas complexos, tais como, previsão meteorológica, clima, dispositivos mecânicos, circuitos electrónicos, processos de fabrico e reacções químicas.
Hoje, contudo, os mais significativos condicionantes do desenvolvimento de computadores de elevado desempenho são as aplicações comerciais emergentes que exigem ao computador a capacidade de processar quantidades substanciais de dados, de forma assaz sofisticada.
Esta aplicações incluem vídeo-conferências, ambientes de trabalho colaborativos, auxiliares ao diagnóstico na área da medicina, bases de dados paralelas usadas para suporte à decisão e gráficos avançados e realidade virtual, particularmente na indústria do entretimento.
Por exemplo, a integração da computação paralela, redes de alta-velocidade e tecnologias multimédia está a levar ao desenvolvimento de servidores de vídeo, computadores projectados para servir centenas ou milhares de pedidos simultâneos de vídeo em tempo real.
Cada sequência de vídeo pode envolver taxas de transferência de dados de muitos mega-octetos por segundo e grandes quantidades de processamento para codificação e descodificação.
Em gráficos, conjuntos de dados tri-dimensionais aproximam-se, agora, de elementos (1024 numa dimensão). A 200 operações por elemento, um ecrã actualizado 30 vezes por segundo requer um computador capaz de
x
operações por segundo.
Apesar das aplicações comerciais poderem vir a definir a arquitecturas da maior parte dos computadores paralelos do futuro, as aplicações científicas tradicionais continuarão a ser utilizadores importantes das tecnologias da computação paralela.
Na verdade, à medida que os efeitos não lineares definem os limites das perspectivas oferecidas pelas investigações puramente teóricas e a experimentação se torna mais cara ou impraticável, os estudos computacionais dos sistemas complexos estão a tornar-se cada vez mais importantes.
Os custos computacionais crescem, tipicamente, na quarta potência, ou mais, da resolução que determina a precisão, assim, estes estudos têm uma visível demanda insaciável por um poder superior de computação.
Estes custos são também caracterizados por requisitos de enormes quantidades de memória e de entradas/saídas.
Por exemplo, uma simulação do clima da terra, para os próximos dez anos, que recorre a um modelo actual, pode envolver operações em vírgula flutuante - dez dias a uma velocidade de execução de
operações de vírgula flutuante por segundo ( 10 giga flops).
Esta mesma simulação pode gerar facilmente um centena de giga-octetos (
octetos), ou mais, de dados.
Ainda assim, como a tabela 1.1 mostra, os cientistas podem facilmente imaginar refinamentos aqueles modelos que fariam crescer estes requisitos computacionais em
vezes.
Estado Actual | Refinamento | Custo |
resolução de 100-km | resolução de 10-km | ![]() ![]() |
representação simples em processos | representação melhorada em processo | 2-10 |
oceano simples | oceano completo | 2-5 |
química atmosférica simples | química atmosférica melhorada | 2-5 |
biosfera limitada | biosfera em compreensão | cerca de 2 |
dezenas de anos | centenas de anos | 10- ![]() |
Este resultado significa que não só é difícil construir componentes individuais para operar mais rapidamente como pode inclusivamente não ser desejável fazê-lo.
Poderá ser mais económico usar um maior número de componentes mais lentos.
Por exemplo, se tivermos uma área
de silício para usar num computador, poderemos, ou construir
componentes, cada um de tamanho A, capazes de efectuar uma operação em tempo T, ou construir um único componente capaz de executar a mesma operação em tempo
.
O sistema multi-componente é, potencialmente, mais rápido
vezes.
Os projectistas de computadores usam uma variedade de técnicas para obviar estas limitações no rendimento de um computador isolado, incluindo encadeamento, pipeline, (diferentes estágios de várias instruções em execução concorrente) e múltiplas unidades funcionais (vários multiplicadores, coprocessadores, etc.), são controlados pelo uma única sequência de instruções). E ainda, os projectistas, estão a incorporar múltiplos ``computadores'', cada um com o seu próprio processador, memória e lógica de interconexão associada. Esta abordagem é favorecida pelos avanços na tecnologia VLSI que continua a fazer decrescer o número de componentes necessários para construir um computador. Como o custo de um computador é (muito aproximadamente) proporcional ao número de componentes que contém, o aumento da capacidade de integração também aumenta o número de processadores que podem ser incluídos num computador para um custo particular. O resultado é o contínuo crescimento no número de processadores (Figura 1.3).
Realçamos o facto de que a computação em computadores em rede (computação distribuída) não é um mero sub-campo da computação paralela. A computação distribuída está profundamente preocupada com problemas tais como a fiabilidade, a segurança e a heterogeneidade que são, em geral, vistas como tangenciais à computação paralela. (Parafraseando Leslie Lamport ``Num sistema distribuído, a falha de um computador que não sabíamos sequer que existia pode tornar inoperável o nosso próprio computador.) Ainda assim, a tarefa de desenvolver programas que podem executar em muitos computadores, ao mesmo tempo, é um problema da computação paralela. A este respeito, a distinção anterior entre os mundos da computação paralela e da computação distribuída são convergentes.
Este levantamento também sugere uma segunda lição fundamental. Parece que o número de processadores continuará a crescer - talvez, como acontece em alguns ambientes presentemente, duplicando em cada um ou dois anos. Assim, os sistemas de software podem presumivelmente vir a ser sujeitos a crescimentos substanciais em número de processadores durante o seu tempo de vida. Neste ambiente, a escalabilidade - a capacidade de adaptação ao aumento do número de processadores - é tão importante como a portabilidade como forma de salvaguardar o investimento em software. Um programa capaz de usar apenas um número fixo de processadores é um mau programa, como o é um programa que só pode ser executado num só computador. A escalabilidade é um tema maior que irá ser enfatizado ao longo deste livro.
O nosso estudo da programação paralela poderá ser, no essencial, compensatório se podermos identificar um modelo tão geral e útil, como o modelo sequencial de Von Neumann. Este modelo de máquina deverá ser ao mesmo tempo simples e realístico: simples para facilitar a compreensão e a programação e realístico para poder assegurar que os programas desenvolvidos para o modelo correm com razoável eficiência nos computadores reais.
O multi-computador é muito semelhante ao que é vulgarmente chamado de computador de memória distribuída MIMD. Esta designação quer significar que cada processador pode executar uma linha separada de instruções sobre os seus dados locais; memória distribuída significa que a memória está espalhada entre processadores ao invés de estar concentrada num local central. A principal diferença entre um multi-computador e um computador MIMD de memória distribuída é que no último, o custos de envio de uma mensagem entre dois nodos pode não ser independente da localização dos nodos e do tráfego na rede. Estes tópicos são discutidos no Capítulo 3. Exemplos desta classe de máquinas incluem o IBM SP, o Intel Paragon, o CM Thinking Machine, o Cray T3D, o Meiko CS-2 e o nCUBE.
Uma outra importante classe de computadores paralelos é o multiprocessador ou computador MIMD de memória partilhada. Nos multiprocessadores, todos os processadores partilham o acesso a uma memória comum, tipicamente, através de um barramento ou de uma hierarquia de barramentos. No modelo idealizado PRAM Máquina Paralela de Acesso Aleatório, frequentemente usado em estudos teóricos de algoritmos paralelos, qualquer processador pode fazer o acesso a qualquer célula de memória na mesma quantidade de tempo. Escalar esta arquitectura, na prática, introduz usualmente alguma forma de hierarquia de memória; em particular, a frequência com que são feitos os acessos à memória pode ser substancialmente reduzida se forem feitas cópias dos dados mais frequentemente usados numa cache associada com cada processador. O acesso a esta cache é muito mais rápido que o acesso à memória partilhada; assim, a localidade é habitualmente importante e as diferenças entre multi-computadores e multi-processadores são, na verdade, questões de escala. Os programas desenvolvidos para multi-computadores podem também ser executados eficientemente nos multi-processadores, porque o modelo por memória partilhada permite a realização eficiente do modelo por passagem de mensagens. Exemplos desta classe de máquinas incluem o Silicon Graphics Challenge, o Sequent Simetry e a maior parte das estações de trabalho multi-processador.
Uma classe mais especializada de computadores paralelos é o computador SIMD, (instrução simples múltiplos dados). Nas máquinas SIMD, todos os processadores executam a mesma linha de instruções sobre diferentes pedaços de dados. Esta abordagem pode reduzir a complexidade, tanto do hardware como do software, mas é, apenas, apropriada para problemas especializados, caracterizados por elevados níveis de regularidade, por exemplo, processamento de imagem e certas simulações numéricas. Os algoritmos para um multi-computador não podem, em geral, ser executados eficientemente em computadores SIMD. O MasPar MP é um exemplo de uma máquina desta classe.
A programação paralela acrescenta fontes adicionais de complexidade; se fossemos programar ao mais baixo nível, não apenas iríamos aumentar o número de instruções, como iríamos, também, ter de gerir explicitamente a execução de milhares de processadores e coordenar milhões de interacções entre processadores. Por isso, a abstracção e a modularidade são, pelo menos, tão importantes como na programação sequencial. De facto, iremos enfatizar a modularidade como o quarto requisito fundamental da programação paralela, a acrescentar à concorrência, à escalabilidade e à localidade.
Uma desvantagem deste esquema é que a fundição pode produzir vigas mais depressa do que os trabalhadores na montagem podem usá-las.
Em alternativa, para evitar que o lugar da ponte fique superlotada com vigas, os trabalhadores na montagem podem encomendar explicitamente mais vigas quando o depósito de vigas baixar de um certo nível. Este refinamento da abordagem é ilustrada na figura 1.9(b), com o fluxo de encomendas representado por um segundo canal. O segundo canal pode, também, ser usado para suspender o fluxo de vigas quando a ponte estiver pronta.
------------------------------------------------
A tarefa é um bloco natural para o desenho modular. Tal como é ilustrado na figura 1.10 uma tarefa esconde tanto os dados como o código que opera sobre esses dados; os portos através dos quais envia e recebe mensagens constituem o seu interface. Assim, as vantagens do desenho modular sumariadas no parágrafo anterior estão directamente acessíveis no modelo tarefa/canal que usado convenientemente pode reduzir a complexidade da programação e facilitar a reutilização de código.
Há uma grande semelhança entre o modelo tarefa/canal e o popular paradigma da programação por objectos. As tarefas, como os objectos, escondem os dados e o código que opera sobre esses dados. Características distintivas do modelo tarefa/canal são a sua concorrência, o uso de canais, em vez de chamada a métodos para especificar a interacção e a falta de mecanismos de hierarquia.
Os instrumentos de interacção suportados pelo próprio modelo tarefa/canal tornam o determinismo relativamente fácil de garantir. Tal como veremos na Parte II quando considerarmos os instrumentos de programação, é suficiente verificar que cada canal tem um único emissor e um único receptor e que a tarefa que está a receber no canal fica bloqueada até que a operação de recepção se complete. Estas condições podem ser relaxadas quando se pretender interacção não-determinística.
Se no exemplo de construção da ponte, o determinismo significar que a mesma ponte virá a ser construída independentemente das velocidade a que a fundição constrói vigas e os operários na montagem juntam as vigas. Se os operários na montagem forem à frente da fundição, terão de ficar bloqueados até que estejam disponíveis mais vigas, em vez de tentarem continuar a construção com um número insuficiente de vigas. De forma semelhante, se a fundição produzir vigas mais depressa do que os operários na montagem poderem usa-las, essas vigas irão simplesmente acumular-se até que sejam necessárias. O determinismo seria garantido mesmo se várias pontes estivessem a ser construídas ao mesmo tempo: enquanto as vigas destinadas a diferentes pontes viajarem por canais distintos, não podem ser confundidas.
O modelo de passagem de mensagens não exclui a criação dinâmica de tarefas, a execução de múltiplas tarefas por processador, ou a execução de diferentes programas por diferentes tarefas.
Contudo, na prática, a maior parte dos sistemas de passagem de mensagens cria um número fixo de tarefas idênticas, no arranque do programa, não permitindo criar ou destruir tarefas durante a execução do programa.
Estes sistemas dizem-se realizar o modelo de programação SPMD (um único programa e múltiplos dados) porque cada tarefa executa o mesmo programa mas opera em dados diferentes.
Tal como é explicado em capítulos subsequentes o modelo SPMD é suficientemente para um largo espectro de problemas de programação paralela mas impede o desenvolvimento de alguns algoritmos paralelos.
Paralelismo nos Dados
Um outro modelo de programação paralela usado comummente, paralelismo nos dados, faz o aproveitamento da concorrência que deriva da aplicação da mesma operação a múltiplos elementos de uma estrutura de dados,
por exemplo, ``somar 2 a todos os elementos de uma lista'', ou `` aumentar o salário de todos os empregados, com cinco anos de serviço''.
Um programa paralelo nos dados consiste de uma sequência de tais operações.
Como cada operação, num determinado elemento de uma estrutura de dados, pode ser pensada como uma tarefa independente, a granularidade da computação paralela nos dados é reduzida, o conceito de localidade dos dados não surge naturalmente.
Assim os compiladores de paralelismo nos dados, muitas vezes, obrigam o programador a fornecer informação sobre a forma como os dados deverão ser distribuídos pelos processadores, por outras palavras, como os dados deverão ser distribuídos entre tarefas.
O compilador pode então traduzir o programa de paralelismo nos dados numa descrição formulada em SPMD, por meio disso, gerando o código de comunicação automaticamente.
Discutimos o modelo de paralelismo nos dados com maior detalhe no capítulo 7 sob o tópico High Performance Fortran.
Naquele capítulo mostramos que o desenho de algoritmos e as técnicas de análise desenvolvidas para o modelo tarefa/canal aplicam-se directamente aos programas de paralelismo nos dados; em particular, fornecem os conceitos necessários para compreender a localidade e a escalabilidade dos programas de paralelismo nos dados.
Memória Partilhada
No modelo de programação por memória partilhada, as tarefas partilham um espaço de endereçamento que lêm e escrevem assincronamente.
Vários mecanismos tais como fechos e semáforos podem ser usados para controlar o acesso à memória partilhada.
Uma vantagem deste modelo do ponto de vista do programador é que falta a noção de ``propriedade'' e, por isso, não é necessário especificar explicitamente a comunicação dos dados do produtor para o consumidor.
Contudo, torna-se muito mais difícil compreender e manipular a localidade, um tópico de grande importância (já antes referido) na maioria das arquitecturas de memória partilhada.
Pode também ser mais difícil escrever programas determinístico.
Os dois primeiros algoritmos descritos têm uma estrutura SPMD, o terceiro cria tarefas dinamicamente durante a execução do programa e o quarto usa um número fixo de tarefas mas tem diferentes tarefas a efectuar funções diferentes.
Um algoritmo paralelo para este problema cria tarefas, uma por cada ponto, em
.
A tarefa
toma o valor
sendo responsável por calcular, em
passos, os valores de
,
, ...,
.
Assim, no passo
, tem de obter os valores de
e
, das tarefas
e
,
Especificamos esta transferência de dados definindo canais que ligam cada tarefa com as vizinhas ``à esquerda'' e ``à direita'', como se mostra na figura 1.11, e requerendo que em cada passo
, cada tarefa
, outra que não a tarefa
e tarefa
Um algoritmo paralelo simples para o problema geral de interacções pares pode criar tarefas.
À tarefa
é entregue o dado
e é responsável por calcular as interacções
Poderíamos ser levados a pensar que como cada tarefa necessita de um dado de toda a outra tarefa, seriam necessários
canais para efectuar as necessárias comunicações.
Contudo, é possível uma estrutura mais económica que usa apenas
canais.
Estes canais são usados para ligar
tarefas num anel uni-direccional (figura 1.12(a)).
Assim, cada tarefa tem uma porta de entrada e uma porta de saída.
Cada tarefa começa por iniciar um tampão (com o valor do seu dado local) e um acumulador que mantém o resultado do cálculo.
Então, repetidamente
Se as interacções forem simétricas, então, podemos reduzir a metade, tanto o número de interacções efectuadas como o número de comunicações através do refinamento da estrutura de comunicações.
Assumamos por simplicidade que é ímpar.
São criados
canais adicionais, ligando cada tarefa à tarefa desviada no anel de
(figura 1.12(b)).
procedimento pesquisa(A)} início se (solução(A) então score = eval(A) reportar solução e pontuação senão para-cada descendente (Ai) de A pesquisar (A(i))} fim-para fim-se fim
Porque este programa usa estruturas de comunicação de muitos-para-um a ordem pela qual os cálculos são efectuados não é necessariamente determinada. Contudo, este não-determinismo afecta apenas a alocação de problemas aos trabalhadores e a ordenação de resultados no ficheiro de saída e não os resultados efectivamente calculados.
O modelo de máquina paralela do multi-computador e o modelo de programação tarefa/canal introduzido neste capítulo irá ser usado em discussões subsequentes de desenho, análise e implementação de algoritmos paralelos. O multi-computador compreende um mais computadores de Von Neumann ligados através de uma rede de interconexão. É um modelo de máquina simples e realista que fornece uma base para o desenho de programas paralelos, escaláveis e portáveis. Um modelo de programação baseado em tarefas e canais simplifica a programação de multi-computadores fornecendo abstracções que nos permitem falar da concorrência, localidade e comunicação, de uma maneira independente da máquina e fornecendo uma base para a construção modular de programas paralelos.
Agora que já discutimos a natureza dos algoritmos estamos, agora preparados para examinar podemos avançar para a análise das formas de os desenhar. Neste capítulo, mostramos como a especificação de um problema é traduzida num algoritmo que exibe concorrência, escalabilidade e localidade. Os temas relacioandos com amodularidade são discutidos no Capítulo 4.
O desenho de algoritmos paralelos não é facilmente reutível a receitas simples. Pelo contrário, requere uma espécie de pensamento integrado, communmente designado por ``criatividade''. Contudo, pode beneficiar de uma abordagem metodológica que maximize a gama de opções consideradas, que forneça os mecanismos necessários à avaliação de alternativas e que reduza os custos de recuperação de más soluções. Apresentamos aquela abordagem e ilustramos com a sua aplicação a uma gama de problemas. Durante o processo esperamos que o leitor desenvolva a intuição do que constitui um bom algorimto paralelo.
Depois de estudar este capítulo, o leitor deverá ser capaz de desenhar algoritmos paralelos simples de uma maneira metódica e reconhecer as nuances de desenho que comprometem a eficiência ou a escalabilidade. Deverá ser capaz de particionar os cálculos, usando amnbas as técnicas de decomprosição em funcões e em domínios e saber reconhecer e implementar estruturas de comunicação, tanto locais como globais, estáticas e dinâmicas, estruturadas e não-estrauturadas, síncronas e assíncronas. Deverá, também, ser capaz de usar o agrupamento como um meio de reduzir os custos de comunicação e de implementação e deverá ficar familiarizado com uma gama de estratégias de balanceameto de carga.
Esta metodologia estrutura o processo de desenho em quatro etapas distintas:partição, comunicação, agrupamento e arranjo. (O acrónimo PCAM pode servir para facilitar a lembrança). Nas duas primeiras etapas, pomos a ênfase na concorrência e na escalabilidade tentamos descobrir algoritmos que possuam estas qualidades. Na terceira e quarta etapas, a atenção é dirigida para a localidade e outros temas relacionados com a eficiência. As quatro etapas são ilustradas na figura 2.1 e pode ser sumariada como segue:
O desenho de algoritmos paralelos é apresentado aqui como uma actividade sequencial. Na prática, contudo, é um processo altamente paralelo, com muitas questões consideradas simultaneamente. Embora, tentemos evitar ter de andar para trás, a avaliação, parcial ou completa, de um desenho pode necessitar de alterar decisões de desenho tomadas em etapas anteriores.
As seccões seguintes apresentam um exame promenorizado das quatro etapas do processo de desenho. Apresentamos princípios básicos, usamos exemplos para ilustrar a aplicação destes proncípios e incluímos listas de testes de desenho que podem ser usadas para avaliar os projectos à medida que são desenvolvidos. Nas secções finais deste capítulo usamos três estudos de casos para ilustrar a aplicação técnicas de desenho a problemas realísticos.
Num projecto a etapa de partição pretende expor as oportunidades para a execução paralela. Assim, a ênfase é posta na definição de um lardo número de pequenas tarfeas de forma a definir o que é designado por grão fino de decomposição de um problema. Tal como a areia fina é mais facilmente vazada do que uma pilha de de tijolos, uma decomposição de grão fino fornece a maior flexibilidade em termos do potencial paralelismo dos algoritmos. Em fases posteriores de desenho, a avaliação das necessidades de comunicação, a arquitectua alvo, ou questões da engenharia do software podem forçar-nos a por de parte oportunidades de execução paralela identificadas nesta etapa. Netss caso, revisitamos a partição originale agrupamos tarefas para aumentar o seu tamanho, ou granularidade. Contudo, nesta primeria etapa, gostariamos de evitar avaliar de antemão estratégias alternativas de partição.
Uma boa partição divide em pequenas partes, tanto a computação associada com um problema e os dados sobre os quais o cálculo é efectuado. Quando se desenha a partição, os programadores, na maior parte dos casos, em primeiro lugar, olham para os dados associados com um problema e, a seguir, determinam uma partição apropriada para os dados e no final trabalham na forma como associar a computação com os dados. Esta técnica de partição é designada por decomposição em domínios. A abordagem alternativa, começa por decompor a computação a ser efectuada e seguidamente trata dos dados - é designada por decomposição em funções. Estas são técnicas complemntares que podem ser aplicadas a a diferentes componentes de um mesmo problema, ou até aplicadas a diferenets mesmo problema, para obter diferentes soluções. Na primeira fase de desenho, procura-se evitar a replicação quer de cálculos quer de dados; ou seja, definem-se as tarefas de forma a dividir tanto os dados como os cálculos em dois conjuntos distintos. Esta opção permite diminuir os requisitos de comunicação.
A utilização desta técnica visa decompor os dados associados a um problema de forma a obter pequenas partes, com tamanhos aproximadamente iguais. Seguidamente procura-se dividir os cálculos a executar, associando a cada operação os dados necessários. Esta divisão produz um certo número de tarefas, cada uma das quais comporta alguns dados e um conjunto de operações sobre estes. Se para efectuar uma operação forem precisos dados que estão noutras tarefas, impõe-se o estabelecimento de comunicações que façam a transferência de dados.
A decomposição em domínios compreende a entrada de dados, a saída de dados, ou a manipulação de dados intermédios durante a execução do programa, o que possibilita a criação de diferentes partições, baseadas em diferentes estruturas de dados. Uma regra básica consiste em definir as partições, dirigindo a atenção para as estruturas de dados maiores ou para as que são alvo de acessos mais frequentes. Se em diferentes fases dos cálculos, se opera em diferentes estruturas de dados ou se se exigem diferentes decomposições da mesma estrutura de dados, então, cada fase deverá ser tratada separadamente, determinando a posterior de que maneira os diferentes algoritmos deverão colaborar. A figura 2.2 ilustra a utilização desta técnica.
Esta técnica põe a ênfase nos cálculos, ao contrário da técnica anterior que se concentra nos dados. Se for possível dividir os cálculos num grupo disjunto de diferentes tarefas podem, a partir daí, examinar-se os requisitos de dados em cada tarefa. Se os dados, por sua vez, se revelarem disjuntos, poderemos concluir que a partição foi realizada com êxito. Se os dados se sobrepõem, então, teremos de considerar que um número significativo de comunicações terão se ser desencadeadas para evitar a replicação de dados. Esta última consideração é o ponto de partida para uma abordagem do problema a realizar com base na técnica anterior.
Apesar da decomposição em domínios ser considerada como fundamental, na maior parte dos algoritmos paralelos, a decomposição em funções pode revelar a estrutura de um problema fornecendo a base para uma optimização que de outra forma permaneceria escondida.
Esta técnica, quando associa a decomposição dos cálculos à decomposição do código executável, permite reduzir a complexidade geral de desenvolvimento de um programa. Este é muitas vezes o caso da modelação de um sistema complexo que pode ser estruturado como uma colecção de modelos mais simples, ligados através de interfaces.
Por exemplo, simulação do clima da terra pode compreender vários componentes representando a atmosfera, o oceano, o dióxido carbónico etc. Enquanto cada componente pode ser naturalmente paralelizável usando DD, o algoritmo paralelo, como um todo, é substancialmente mais simples se o sistema for inicialmente dividido usando DF, mesmo que este abordagem não produza um número significativo de tarefas.
Apresentamos em seguida um lista de perguntas, que deverão normalmente ser respondidas afirmativa mente, que asseguram que o desenho não contém falhas óbvias:
A resposta às questões formuladas acima, poderá sugerir que apesar de se ter cuidadosamente pensado numa partição, as etapas posteriores de desenho podem levar a concluir que a solução não é boa. Nesta circunstância, há um grande risco em prosseguir com a realização do modelo encontrado. A avaliação de rendimento surge então como uma técnica que irá permitir determinar se o desenho corresponde aos objectivos pretendidos apesar das aparentes deficiências. Como alternativa pode-se sempre revisitar a especificação do problema para determinar se existente métodos numéricos, que simplifiquem e optimizem o desenvolvimento da solução final.
As tarefas definidas por uma determinada partição poderão executar concorrentemente mas podem, em geral, não ter execução independente. Tipicamente, os cálculos efectuados por uma tarefa precisam de dados associados a outras tarefas. Nesta circunstância, os dados deverão ser transferidos entre tarefas de forma a possibilitar a continuação do cálculo.
No desenho de um algoritmo esta informação é especificada na fase de comunicação. À comunicação faz-se corresponder duas fases:
Depende da tecnologia de realização utilizada a criação, ou não, desses canais durante a codificação do algoritmo. A definição de um canal tem custos intelectuais e físicos associados, por essa razão, é de evitar a criação de canais e comunicações desnecessárias. A procura de um rendimento óptimo, acrescenta ainda a necessidade de definir e organizar operações de comunicação, distribuídas entre várias tarefas, de tal forma que se garanta a execução concorrente.
Se a decomposição é em domínios, poderão ser dificéis de determinar os requisitos de comunicação. Com efeito, quando uma operação precisa de dados que estão em diferentes tarefas, poderá ser difícil organizar as comunicações de forma a que a transferência de dados se produza de forma eficiente. Em contraste com esta perspectiva, a comunicação associada à decomposição em funções corresponde muitas vezes ao fluxo de dados entre tarefas.
Na discussão que se segue pretende-se mostrar como são identificados os requisitos de comunicação e as operações de comunicação necessárias à sua satisfação. Para facilitar a exposição, consideraremos os seguintes eixos de discussão: local/global, estruturado/não-estruturado, estático/dinâmico e assíncrono/síncrono.
Uma estrutura de comunicação local resulta de uma operação que precisa de dados de um pequeno número de outras tarefas. Nesta circunstâncias é imediata a definição de canais que liguem as tarefas responsáveis pela operação (consumidores) às tarefas que produzem os dados (produtoras) e a criação de operações apropriadas de recepção e envio, respectivamente.
Consideremos os requisitos de comunicação associados um cálculo simples
numérico, o método das diferenças finitas de Jacobi.
Neste tipo de problemas uma grelha multidimensional é repetidamente actualizada
substituindo o valor em cada ponto, pelo valor de uma função aplicada a um
pequeno número fixo de pontos.
Ao conjunto de valores necessários para actualizar um ponto da grelha dá-se o
nome de pontos de matriz.
Se usarmos uma matriz de cinco pontos para cada valor duma grelha
bidimensional
, obtemos:
Enquanto a técnica Jacobi é facilmente paralelizável (todos os pontos podem ser actualizados concorrentemente) o mesmo não se passa com a técnica anterior. Felizmente, podem ser usadas ordens de actualização variadas, o que dá um certo grau de liberdade, na escolha da ordem que maximiza o paralelismo existente.
Este exemplo ilustra a importância da estratégia de solução na determinação da eficiência de um programa paralelo.
A fórmula é aplicada repetidamente para calcular todos os valores de , sendo
a notação
usada para referir o valor do ponto
da grelha
no passo
.
Se usarmos uma partição, com base numa decomposição em domínios, para criar uma
tarefa para cada cada ponto na grelha, então, uma tarefa atribuída ao ponto
deverá efectuar
passos sequenciais.
E para cada passo deverão ser calculados todos os elementos no numerador da
fórmula acima, isto é, por cada uma das quatro tarefas vizinhas.
Para que todos estes valores sejam conhecidos definem-se para cada tarefa,
tantos canais quanto o número de vizinhos, Fig 2.4.
para até
send para cada vizinho
receive
dos vizinhos
calcule usando a equação anterior
parafim
Tal como anteriormente foi dito o melhor algoritmo sequencial e a solução
paralela podem usar diferentes técnicas.
Em computação sequencial, a técnica de Gauss-Seidel é usada preferencialmente
porque se obtêm soluções de precisão comparável, mas usando menos iterações.
Neste caso, a equação anterior pode ser reformulado dando lugar a:
Este tipo de operação corresponde a uma comunicação que envolve muitas tarefas.
Neste caso pode não ser suficiente identificar os pares consumidor/produtor,
porque isso resultaria em demasiadas comunicações, ou poderia impôr uma restrição à oportunidade para a execução concorrente.
Consideremos, por exemplo, uma operação paralela de redução, isto é,
uma operação que reduz valores distribuídos por
tarefas usando um
operador associativo de adição:
Se assumirmos que uma simples tarefa mestre precisa do resultado da
operação, então, do ponto de vista estritamente local essa tarefa precisa dos
valores
etc... das tarefas
etc.
Isto, pode corresponder à definição de uma estrutura que torne possível a
comunicação independente, entre cada tarefa e a tarefa mestre, sendo esta
responsável pela adição dos valores e armazenamento do resultado.
Contudo, porque a tarefa mestre apenas pode somar um valor de cada vez, a
solução requere um tempo de
para adicionar os
números o que,
convenhamos, não parece ser uma boa solução paralela.
O exemplo ilustra dois problemas gerais, que se baseiam numa visão puramente local da comunicação que podem prejudicar a execução eficiente de algoritmos paralelos:
As comunicações necessárias a este algoritmo deverão ser satisfeitas ligando
as tarefas numa fila, Fig 2.7.
A tarefa
envia o seu valor para o seu vizinho na fila.
Cada uma das tarefas
a
espera até receber a soma parcial do vizinho
da direita, soma esse valor ao seu próprio valor e envia o resultado para o
vizinho da esquerda.
A soma global é calculada pela tarefa
adicionando o seu próprio valor ao
valor parcial, recebido da tarefa à direita.
O algoritmo distribui as
comunicações e somas, mas só permite a execução
concorrente se o problema incluir múltiplas operações de soma.
Nesta circunstância, a fila de tarefas pode ser usada como uma cadeia, através
da qual fluem as somas parciais, que realiza uma soma simples em
passos.
As oportunidades para a execução e a comunicação concorrente podem muitas vezes ser
expostas aplicando aos problemas uma estratégia de
Dividir para Paralelizar.
Para resolver um problema complexo ( tal com a soma de números) procura-se
dividi-lo em dois ou mais problemas de tamanho idêntico (somar
números).
Este processo é então aplicado recursivamente, até atingir um conjunto de
subproblemas não subdivisíveis (soma de dois números).
Esta técnica descrita no algoritmo 2.1 é efectiva em processamento paralelo
quando os subproblemas podem ser resolvidos concorrentemente.
Se tomarmos como exemplo o a soma de
números pode aproveitar-se a seguinte
equação
inteiro:
Sumarizando, para obter um algoritmo suficientemente eficiente foram
distribuídos os cálculos e comunicações e modificada a ordem de execução,
de forma a cada um poder prosseguir concorrentemente.
O resultado é uma estrutura regular de comunicação, em que cada tarefa comunica
com um pequeno número de tarefas vizinhas.
A comunicação não estruturada não causa dificuldades conceptuais especiais nas fases iniciais de desenho. O mesmo não acontece em fases mais adiantadas quando se pretende, por exemplo, agrupar ou fazer o arranjo dos processadores. Em particular, pode ser necessário desenvolver algoritmos sofisticados, para determinar a estratégia de agrupamento que crie tarefas de tamanhos aproximadamente iguais e que, ao mesmo tempo, minimize as comunicações, por redução do número de tarefas interactuantes. A juntar a estas dificuldades, se os requisitos de comunicação tiverem carácter dinâmico, há ainda que considerar, se a relação custo/benefício justifica a sobrecarga resultante da execução frequente dos algoritmos.
A necessidade de comunicação assíncrona entre tarefas ocorre frequentemente, em situações em que um conjunto de tarefas tem que fazer acesso, periodicamente, aos elementos de uma estrutura partilhada de dados . Se considerarmos que essa estrutura de dados é demasiadamente grande ou é alvo de acessos, com frequência suficientemente elevada para não justificar o encapsulamento numa única tarefa, o mecanismo capaz de garantir operações assíncronas de leitura e escrita e a distribuição da estrutura de dados deverá assegurar que:
Cada estratégia possui as suas próprias vantagens e desvantagens e o rendimento depende ainda da máquina disponível.
Apresentamos em seguida um lista de perguntas, que deverão normalmente ser respondidas afirmativamente, que não têm como propósito encontrar regras rigorosas e imediatas, mas identificar características não escaláveis.
Nas duas primeiras etapas de desenho, a computação foi dividida num conjunto de tarefas e foi introduzida a comunicação, como meio de fornecer os dados necessários às tarefas. O algoritmo, assim conseguido, continua a ser abstracto no sentido em que não foi desenhado para a execução eficiente num computador específico. De facto, pode até ser altamente ineficiente se, por exemplo, criar mais tarefas do que processadores disponíveis e o computador não tiver sido projectado para a execução eficiente de um pequeno número de tarefas.
Com o agrupamento pretende-se passar do mundo abstracto para a realidade, revendo as decisões feitas nas fases anteriores, de forma a obter um algoritmo que execute eficientemente numa classe específica de computadores paralelos. Considera-se, em particular, a utilidade de combinar ou agrupar as tarefas, identificadas na fase de partição, num grupo mais pequeno de tarefas de maior tamanho Fig 2.11 e determinar até que ponto vale a pena replicar dados e/ou cálculos.
O número de tarefas que resulta desta fase poderá ser, apesar de pequeno, ainda
superior ao número processadores.
Neste caso, o algoritmo continua a ser de certa forma abstracto na medida em
as questões relativas ao arranjo dos processadores permanece por resolver.
A alternativa poderá ser agrupar as tarefas, de forma a conseguir obter um
número exactamente igual de tarefas e processadores.
Este será o caso, que leva a considerar concluído o desenho do algoritmo, se o
computador ou o ambiente de programação exigir um programa SPMD; o arranjo já
está resolvido se as tarefas executam em
processadores.
As decisões relativas ao agrupamento e replicação podem por vezes gerar conflitos se os objectivos são:
Encontrar, na fase de partição, o maior número possível de tarefas é uma disciplina útil que nos força a considerar uma vasta gama de hipóteses de execução paralela. Convém no entanto notar que a definição de um número elevado de tarefas não conduz necessariamente à obtenção de um algoritmo eficiente.
Um ponto crítico que influencia largamente a eficiência são os custos de comunicação. Em muitos computadores é preciso suspender a computação para permitir enviar e receber mensagens. Como na prática o que importa é a computação o aumento de eficiência pode ser conseguido reduzindo o tempo de comunicação. Obviamente que podem ser obtidos resultados idênticos, transferindo menos dados, ou usando menos mensagens, mesmo mantendo fixo o volume total de dados. Esta última consideração resulta da existência de um custo fixo que não depende apenas do volume total de dados, mas do próprio mecanismo de comunicação. Um outro valor a contabilizar é o custo de criação das tarefas.
Se o número de participantes numa comunicação for pequeno, pode reduzir-se tanto o número de operações de comunicação, como aumentar o volume global de comunicações, aumentando a granularidade da partição, isto é, agrupando várias tarefas numa única tarefa. No exemplo, Fig. 2.12, a redução dos custos de comunicação é obtida através do efeito superfície-volume. Os requisitos de comunicação de uma tarefa são proporcionais à superfície do subdomínio em que opera, enquanto que os requisitos de computação são proporcionais ao volume do subdomínio. Num problema bidimensional a superfície é escalável em função do tamanho do problema, enquanto o volume é escalável, no quadrado do tamanho do problema. Assim, o aumento da comunicação por unidade de computação (rácio comunicação/computação) diminui à medida que o tamanho das tarefas aumenta. Este efeito é apenas visível quando a partição é obtida usando técnicas de decomposição em domínios.
Uma consequência do efeito superfície-volume é uma elevada decomposição dimensional que produz, tipicamente, uma elevada eficiência, porque reduz a superfície comunicacional requerida, para um determinado volume de computação. Assim, do ponto de vista da eficiência, é normalmente preferível aumentar a granularidade, agrupando tarefas em todas as dimensões, ao invés de reduzir a dimensão da decomposição. O desenho de uma estratégia de agrupamento eficiente pode ser muito dificultada, em problemas em que a comunicação é não estruturada, o que obriga a usar técnicas especializadas.
Pode ser feita uma abordagem simples à distribuição da soma se se usar em
primeiro lugar um algoritmo baseado numa estrutura em anel ou árvore, para
calcular a soma numa única tarefa e seguidamente difundir, a soma para
cada uma das tarefas.
A difusão pode usar a mesma estrutura de comunicação do somatório; assim, a
operação completa pode ser executada em
ou
passos, dependendo da estrutura de comunicação usada.
Estes algoritmos são óptimos na medida em que não é realizada qualquer comunicação ou computação desnecessária. A ideia básica é efectuar múltiplas somas concorrentemente, com cada adição produzindo um valor numa tarefa diferente.
Começaremos por considerar uma variante do algoritmo de soma em fila, em que
as tarefas são ligadas em anel e executam o mesmo algoritmo, de forma
a que as somas parciais estejam a ser calculadas simultaneamente.
Depois de passos a soma aparece replicada em todas as tarefas.
Esta estratégia evita subsequentes operações de difusão, à custa de
adições redundantes e
comunicações desnecessárias.
Contudo, a soma, tal como a difusão completam-se em
passos em vez de
passos, o que leva a concluir que esta estratégia é mais eficiente do
que se os processadores estivessem à espera dos resultados da soma.
O algoritmo da soma em árvore pode ser modificado, de forma idêntica, para
evitar difusões em separado.
Neste caso, múltiplas somas na árvore são executadas concorrentemente e que
depois de passos, cada tarefa tenha uma cópia da soma.
O resultado será, tal como no caso do algoritmo em anel, adições e comunicações
de
.
Contudo, neste caso, a redundância, tanto em computação como em comunicação,
pode ser explorada para executar a soma em apenas
operações.
A estrutura de comunicações resultante, borboleta fig. 2.14, mostra
que em cada um dos
passos, cada tarefa recebe dados de duas tarefas,
executa apenas uma adição e envia o resultado dessa adição para duas tarefas,
no passo seguinte.
O agrupamento é quase sempre benéfico se a análise dos requisitos de comunicação revelar que um determinado conjunto de tarefas não pode ser executado concorrentemente. Consideremos, por exemplo, as estruturas em árvore das figs. 2.8 e 2.14. Sempre que uma adição simples é efectuada, apenas as tarefas ao mesmo nível, numa dessas estruturas, pode ser executada concorrentemente. Convém no entanto notar que se houver muitas adições a efectuar, então, em princípio, todas as tarefas podem ser mantidas em actividade, canalizando múltiplas operações de soma. Assim, as tarefas nos diferentes níveis podem ser agrupadas, sem que se reduzam as oportunidades para a execução concorrente, o que produz a estrutura de comunicações na fig. 2.15 O hiper-cubo, nesta figura, é uma estrutura de comunicações fundamental usada em muitas aplicações da computação paralela.
Quando se agrupam tarefas é fácil tomar decisões que limitam desnecessariamente a escalabilidade dos algoritmos. Podemos tomar, por exemplo, a decisão de decompôr uma estrutura de dados multidimensional, numa única dimensão, com a justificação de que essa opção garante concorrência, mais do que a necessária, para o número de processadores disponíveis. Contudo, esta estratégia é de vistas curtas, se em última análise, o programa for para levar para um computador paralelo maior, podendo ainda conduzir a um algoritmo menos eficiente.
A habilidade para criar um número variável de tarefas é um ponto crítico, se se pretender que um programa possa ser escalável e transportável. Um bom algoritmo paralelo deverá ser adaptável à alteração do número de processadores. Esta flexibilidade pode ser também de grande utilidade quando se pretende afinar o código para um computador específico. Se as tarefas bloqueiam frequentemente, à espera de dados remotos, pode ser vantajoso arranjar várias tarefas num único processador. Assim, uma tarefa bloqueada não quer dizer um processador ocioso, uma vez que uma outra tarefa poderá estar pronto para ser actividade, no seu lugar. Esta técnica é vulgarmente chamada de sobreposição da comunicação com a comunicação.
Um terceiro benefício, da criação de mais tarefas do que processadores disponíveis, é que, desta maneira, se aumenta a gama de estratégias de arranjo que permitem o balanceamento da carga computacional pelos vários processadores disponíveis. Pode dizer-se, como regra de mão, que o número de tarefas deverá exceder, numa ordem de grandeza, o número de processadores.
O número óptimo de tarefas é, tipicamente, determinado pela combinação da modelação analítica com o estudo empírico. A flexibilidade não pressupõe necessariamente a criação de um elevado número de tarefas, porque a granularidade pode ser controlada durante a compilação ou através de um parâmetro de evocação do programa. O que é verdadeiramente importante no desenho de um algoritmo é não limitar, desnecessariamente, o número de tarefas que podem ser criadas.
Estas duas estratégias geram frequentemente conflitos, pelo que se torna necessário estimar os respectivos custos de aplicação. Para além disso, a limitação de recursos pode restringir o número de tarefas que podem ser alocadas a um único processador.
O problema do arranjo é conhecido como -completo, querendo com isto dizer,
não existe nenhum algoritmo que, em tempo polinomial, possa concluir qual o
melhor arranjo possível.
Contudo, existem actualmente, para um determinada classe de problemas, um
considerável conhecimento de técnicas especializadas e heurísticas efectivas.
Muitos algoritmos desenvolvidos a partir de técnicas de decomposição em
domínios exibem um número fixo e do mesmo tamanho, de tarefas e de comunicações
estruturadas, locais e globais.
Nestes casos, é imediata a obtenção de um arranjo eficiente que corresponda à
minimização da comunicação entre processadores fig. 2.16; pode também escolher-se, agrupar tarefas alocadas ao mesmo processador, até atingir um total de
tarefas de médio grão, por cada processador.
Em algoritmos mais complexos, de decomposição em domínios, com quantidades variadas de trabalho por tarefa e/ou padrões de estruturas de comunicação não estruturada, poderão ter de se empregar, normalmente com base em heurísticas, técnicas de balanceamento de carga que procurem identificar agrupamentos eficientes e estratégias de arranjo.
O tempo necessário para executar estes algoritmos deverá ser contabilizado face aos benefícios da redução do tempo de execução. Balanceamento de carga probabilísticos tenderão a causar menos sobrecarga do que os métodos que exploram a estrutura da aplicação. Os problemas mais complexos são aqueles em que tanto o número de tarefas como a grandeza da computação e/ou da comunicação variam dinamicamente durante a execução de um programa. No caso de desenvolvimentos que se baseiam em técnicas de decomposição em domínios, podem usar-se estratégias de balanceamento de carga dinâmico que se baseiam num algoritmo de balanceamento de carga, executado periodicamente, para determinar um novo agrupamento e um novo arranjo. Porque o balanceamento de carga tem de ser executado muitas vezes, durante o tempo de vida de um programa, podem ser preferíveis algoritmos locais que não dependam de um conhecimento global do estado actual do programa.
Os algoritmos que se baseiam em técnicas de decomposição em funções produzem, frequentemente, muitas tarefas pequenas e de curta duração que sincronizam com outras tarefas apenas no inicio e no fim da sua actividade. Neste caso, podem usar-se algoritmos de escalonamento de tarefas que irão atribuir as tarefas aos processadores que estão ociosos ou que estão em vias de se tornar ociosos.
A mais imediata das técnicas de bissecção, a bissecção coordenada
recursiva, é aplicada, normalmente, a matrizes irregulares que têm sobretudo
uma estrutura de comunicação local.
Esta técnica estabelece as divisões com base nas coordenadas físicas dos pontos
no domínio da matriz.
Em cada passo a divisão é feita ao longo da maior das dimensões, p.e.
a divisão é feita ao longo do eixo dos , pelo que, os pontos num subdomínio
terão a ordenada
maior que os pontos nos outros subdomínios.
Esta abordagem tem a vantagem de ser simples e económica conduzindo a bons resultados de divisão computacional. Uma desvantagem é que não optimiza a eficiência das comunicações. Em particular, pode gerar subdomínio poucos densos, o que, no caso de algoritmo prever um número significativo de comunicações locais resultará em mais mensagens das que seriam produzidas por uma decomposição que gerasse subdomínios quadrados.
Uma variante desta técnica, a bissecção recursiva não balanceada,
promove a redução dos custos de comunicação formando submatrizes com melhores
rácios.
Em vez de dividir mecanicamente uma matriz em duas, começa por considerar as
partições obtidas formando submatrizess não-balanceadas com
e
da carga, com
e
da carga e assim sucessivamente,
escolhendo a partição que minimiza o rácio de partições.
Este método aumenta os custos de cálculo da partição mas pode reduzir os custos
de comunicação.
Uma outra técnica, designada por bissecção por grafo recursivo, pode ser
de grande utilidade em estruturas complexas de matrizes não estruturadas,
como é o caso de uma rede de elementos finitos.
Esta técnica usa informação sobre a conectividade, para reduzir o número de
lados da matriz que cruzam limites de subdomínio, reduzindo desta forma os
requisitos de comunicação.
Uma matriz é tratada como um grafo de vértices (pontos da matriz)
.
O algoritmo começa por identificar as duas extremidades do grafo, i.e., os dois vértices que estão mais distantes em termos da distância no grafo.
Seguidamente, cada vértice é entregue ao subdomínio correspondente à
extremidade mais perto.
Em muitas circunstâncias, um outro algoritmo, chamado bissecção recursiva
espectral, é, em muitas circunstâncias, ainda melhor.
As técnicas que acabamos de descrever são relativamente pouco económicas porque requerem um conhecimento global do estado da computação. Em contraste, os algoritmos de balanceamento locais compensam as modificações na carga computacional usando apenas a informação obtida de um pequeno número de processadores vizinhos. Por exemplo, os processadores podem estar organizados numa trama lógica; periodicamente, cada processador compara a sua carga computacional com a carga dos seus vizinhos na trama e transfere a computação, se a diferença em carga exceder um determinado limiar. A fig 2.17 mostrar a distribuição de carga produzida por um desses esquemas.
Porque são económicos, os algoritmos locais podem ser de grande utilidade em situações em que a carga varia constantemente. Contudo, são tipicamente menos bons, no balanceamento de carga, que os algoritmos globais e, em particular, podem ser lentos, no ajuste a grandes alterações nas características da carga. Por exemplo, se aparecer um valor elevado de carga num processador, são necessários múltiplas operações de balanceamento de carga local, antes que a carga seja difundida pelos outros processadores.
O aspecto mais crítico e complicado do algoritmo de escalonamento é a estratégia usada para entregar os problemas às tarefas trabalhadoras. Em geral, a estratégia escolhida representa um compromisso entre os conflitos gerados pela necessidade de operações independentes (para reduzir os custos de comunicação) e o conhecimento global do estado da computação (para melhorar o balanceamento de carga).
Existe uma grande variedade de políticas de acesso. Por exemplo, um trabalhador pode pedir trabalho de um pequeno número, pré-definido, de vizinhos ou pode seleccionar, ao acaso, outros processadores. Num esquema híbrido centralizado/distribuído, os pedidos são enviados ao gerente principal que os distribui pelos trabalhadores de modo circular. Notar que, apesar de o gerente gerar um engarrafamento se houver um grande número de processadores, terá, de qualquer forma, acessos menos frequentes do que no caso escalonamento gerente/trabalhador, sendo, por isso, um construtor mais escalável.
Tal como já foi anteriormente referido, o acesso a uma estrutura de dados distribuída, tal como os lotes de tarefas no esquema de balanceamento de carga descentralizado, pode ser obtido de muitas formas diferentes. Os trabalhadores podem ser responsáveis tanto pela computação como pela comunicação, ao mesmo tempo, que gerem a fila de problemas. Neste caso, cada trabalhador deve inquirir a existência de pedidos pendentes. Alternativamente, as responsabilidade de computação e de gestão deverão estar embebidas em tarefas separadas.
Na programação paralela, como noutra qualquer disciplina de engenharia, o objectivo do desenho não é optimizar apenas uma medida tal como o ganho. Pelo contrário, um bom desenho deverá optimizar uma função de custo específica do problema: tempo de execução, requisitos de memória, custos de realização, custos de manutenção, etc. Esta optimização do desenho envolve compromissos entre simplicidade, rendimento, portabilidade e outros factores.
Tomar decisões fundamentadas de desenho a partir de hipóteses alternativas requer a compreensão dos custos envolvidos. Neste capítulo, mostramos de que maneira este conhecimento pode ser desenvolvido e formalizado em modelos de rendimento matemáticos. Estes modelos podem ser usados para comparar a eficiência de diferentes algoritmos, para avaliar a escalabilidade e para identificar congestionamentos e outras ineficiências, tudo isto, antes que investamos um esforço substancial na implementação. Os modelos de rendimento podem também ser usados para orientar os esforços de implementação mostrando onde se deve proceder a optimizações.
Depois de estudar este capítulo, o leitor deverá saber como desenvolver modelos de rendimento para algoritmos paralelos e ser capaz de usar estes modelos para avaliar a escalabilidade e escolher entre algoritmos alternativos. Deverá ainda saber como obter dados empíricos fiáveis e como usar estes dados para validar os modelos e as suas implementações. E, ainda, compreender como é que a topologia da rede pode afectar o rendimento dos modelos de comunicação e saber como entrar em consideração com esses efeitos nos modelos. Finalmente, deverá ser capaz de reconhecer e contabilizar outros factores, para além do rendimento, factores tais como os custos de implementação que influenciam as escolhas de desenho.
A importância relativa das diversas métricas irá variar conforme a natureza do problema em mãos. Uma especificação pode fornecer números rigorosos para algumas métricas, impor a optimização de outras, ou até ignorá-las. Por exemplo, a especificação de desenho de um sistema operacional de previsão de clima pode estabelecer o tempo de execução máximo (``a previsão deverá estar completada em 4 horas''), os custos do equipamento e os custos de realização e requerer que a fiabilidade do modelo seja maximizada dentro desses limites. Cumulativamente, a fiabilidade é de importância particularmente elevada, tal como pode ser a escalabilidade para futuras gerações de computadores.
Em contraste, um grupo de engenheiros que estão a desenvolver um programa de pesquisa numa base de dados, para o seu próprio uso ocasional, podem ficar satisfeitos com qualquer coisa que corra mais depressa do que um programas sequencial já existente, mas podem ficar extremamente constrangidos pelo tempo que é necessário gastar na sua implementação. Aqui, a escalabilidade é menos crítica, mas o código deverá adaptar-se facilmente às alterações tanto do sistema de bases de dados, como das tecnologias dos computadores.
Como um terceiro exemplo, consideremos uma cadeia de processamento de imagens consistindo de vários estágios concorrentes, cada um executando uma transformação diferente numa sequência de imagens. Aqui, devemos estar preocupados não com o tempo total requerido para processar um certo número de imagens mas, em vez disso, com o número de imagens que podem ser processadas por segundo (desempenho), ou com o tempo que uma simples imagem demora a passar através da cadeia (tempo de execução). O desempenho seria importante numa aplicação de compressão de imagens vídeo, enquanto que o atraso seria importante se o programa fizesse parte de um sistema de detecção que tem que reagir, em tempo real, aos eventos detectados numa série de imagens.
Noutras situações, o rácio tempo de execução custo do sistema pode ser importante. Por exemplo, consideremos um banco que gasta todas as noites duas horas do tempo do seu sobrecarregado computador central para correr um programa de análise à procura de transações fraudulentas. Uma versão que corra em seis horas num computador paralelo que custa um vigésimo do preço, pode constituir uma solução bem mais interessante mesmo que o tempo de execução total seja superior.
Grande parte do material do resto do capítulo está relacionado com a modelação e a medição de apenas dois aspectos do rendimento de algoritmos: o tempo de execução e a escalabilidade paralela. Enfatizamos estes tópicos porque estes estão frequentemente entre os aspectos mais problemáticos do desenho de programas paralelos e porque estes são mais facilmente formalizáveis em modelos matemáticos. Contudo, estes tópicos devem ser examinados no contexto de um processo de desenho mais abrangente que também contemple os restantes temas listados nesta secção.
Nos primórdios da computação paralela acreditava-se que este efeito limitaria a utilidade da computação paralela a um pequeno número de aplicações especializadas. Contudo, a experiência prática demonstra que esta maneira inerentemente sequencial de pensar é de pequena relevância em problemas reais. Para compreender porquê, consideremos um problema não computacional.
Se assumirmos que dos
trabalhadores de um projecto de construção de uma auto-estrada estão sem fazer nada, enquanto um único trabalhador completa uma ``componente sequencial'' do projecto. Não iremos ver isto como uma propriedade intrínseca do problema a ser resolvido, mas a uma falha de gestão.
Por exemplo, se o tempo necessário a um camião para despejar betão num único ponto provoca congestionamento, podemos sempre argumentar que a estrada deveria estar a ser construída em vários pontos, simultaneamente.
Fazendo isto, introduz-se sem margem para dúvidas alguma ineficiência - por exemplo, alguns camiões teriam de viajar mais longe para chegar ao seu local de trabalho -- mas iria permitir que a empreitada no seu conjunto terminasse mais rapidamente.
Da mesma forma, parece que quase todos os problemas computacionais admitem soluções paralelas.
A escalabilidade de algumas da soluções pode estar limitada mas, isto é devido aos custos de comunicação, aos tempos de espera, ou à replicação da computação, mais do que à existência de `` componentes sequenciais''.
A lei de Amdhal pode ser relevante quando os programas sequenciais são paralelizados incrementalmente. Nesta abordagem ao desenvolvimento de programas paralelos, um programa sequencial é inicialmente analisado para identificar os componentes com requisitos computacionais. Esses componentes são depois adaptados para execução paralela, um após outro, até se atingir uma rendimento aceitável. A lei de Amdahl aplica-se claramente nesta situação, porque os custos da computação dos componentes que não são paralelizáveis determinam o limite inferior do tempo de execução do programa paralelo. Por isso, esta estratégia de paralelização, ``parcial'' ou ``incremental'', é geralmente efectiva apenas em pequenos computadores paralelos. A lei de Amdahl pode, também, ser útil quando se analisa o rendimento de programas com paralelismos nos dados, em que alguns componentes podem não ser adaptáveis a uma formulação de paralelismo nos dados (ver capítulo 7).
Presumivelmente, este ponto único de dados, num pequeno número de processadores, pretende ser uma medida da qualidade do algoritmo.
Um ganho de em
processadores pode ser, ou não ser, entendido como sendo ``bom''.
Contudo, uma única medição de rendimento (ou mesmo várias medições) serve apenas para determinar o rendimento numa região estreita do que é um vasto espaço multi-dimensional e é, muitas vezes, um indicador pobre de rendimento noutras situações. O que acontece com
processadores? E se
ou
? E se os custos de comunicação forem dez vezes superiores?
Responder a estas questões obriga a uma compreensão mais profunda do algoritmo paralelo.
As três equações que se seguem põem a enfâse nas limitações da observação como um instrumento para a compreensão do rendimento paralelo. Cada uma é um modelo de rendimento simples que especifica o tempo de execução como uma função do número de processadores
e do tamanho do problema
.
Em cada um dos casos, assumimos que o tempo total de computação executado por um algoritmo sequencial optimizado aumenta com
.
Isto é, existe um valor constante e um tamanho mínimo
, tal que, para todo o
, o
, em
processadores. Esta relação mostra de que forma o custo varia com
quando
e
são grandes.
Apesar desta informação ser interessante, é muitas vezes não directamente relevante para a tarefa de desenvolver um programa paralelo eficiente.
Porque diz respeito a valores elevados de e
, ignora os termos de baixa ordem que podem ser significativos para dimensões de problemas e números de processadores com interesse prático.
Por exemplo, o custo efectivo de um algoritmo com complexidade assimptótica
pode ser
.
O componente
é maior para
e deverá ser incorporado no modelo de rendimento se existirem problemas interessantes, neste regime.
Uma segunda deficiência da análise assimptótica é que nada é dito sobre os custos absolutos.
A análise assimptótica sugeriria que um algoritmo com custo
é superior a um algoritmo com custo
. Contudo, este último é mais rápido para
que de novo pode ser o regime de interesse prático.
Uma terceira deficiência é que estas análises assumem frequentemente modelos idealizados de máquinas que são muito diferentes dos computadores físicos para os quais desenvolvemos programas. Por exemplo, aqueles podem assumir o modelo PRAM3.3 , no qual os custos de comunicação são considerados nulos.
A análise assimptótica tem um papel a desempenhar no desenho de programas. Contudo, quando se avaliam os resultados assimptóticos, devemos identificar cuidadosamente o modelo de máquina para os quais os resultados são obtidos, os coeficientes apropriados para aplicar, e o regime para o qual os valores de e
se verificam.
Os modelos de rendimento considerados aqui especificam uma métrica, tal como o tempo de execução, , como uma função do tamanho do problema
, do número de processadores
, do número de tarefas
e outras características algorítmicas e físicas.
Esta última definição é muitas vezes mais útil, uma vez que, é tipicamente mais fácil determinar a computação e a comunicação totais, efectuada por um algoritmo paralelo, do que o tempo gasto em computação e comunicação em processadores individuais.
Assim, o objectivo é desenvolver expressões matemáticas que especificam o tempo de execução como uma função de , de
, etc.
Estes modelos deverão ser tão simples quanto possível, sem deixarem de ter uma precisão aceitável. Usamos as seguintes técnicas para reduzir a complexidade dos modelos:
Começamos por examinar os três componentes do tempo total de execução: o tempo de computação, o tempo de comunicação e o tempo de ócio.
Tempo de Computação
O tempo de computação de um algoritmo () é o tempo gasto a efectuar cálculos sem contar a comunicação e a ociosidade. Se tivermos um programa sequencial que executa a mesma computação que um algoritmo paralelo podemos determinar o
cronometrando aquele programa.
Doutra forma, temos que realizar chaves de medição directa.
O tempo de computação depende, normalmente, em alguma medida, do tamanho do problema, quer o tamanho seja representado por um único parâmetro ou por um conjunto de parâmetros
.
Se o algoritmo paralelo replicar a computação, então o tempo de computação depende também do número de tarefas ou de processadores.
Num computador paralelo heterogéneo (tal como uma rede de estações de trabalho) o tempo de computação pode variar em função do processador em que a computação é efectuada.
O tempo de computação depende, também, das características dos processadores e dos sistemas de memória. Por exemplo, a variação do tamanho do problema ou do número de processadores pode alterar a rendimento da cache ou a eficiência do encadeamento dos processadores. Como consequência, não é possível assumir, automaticamente, que o tempo total de computação se mantém constante à medida que se altera o número de processadores.
Tempo de Comunicação
O tempo de comunicação de um algoritmo () é o tempo que as tarefas gastam a enviar e a receber mensagens.
Podem distinguir-se dois tipos diferentes de comunicação: a comunicação inter-processadores e a comunicação intra-processador.
Na comunicação inter-processadores, as duas tarefas em comunicação estão localizadas em diferentes processadores. Este será sempre o caso se um algoritmo cria uma tarefa por processador. Na comunicação intra-processador, as duas tarefas em comunicação estão localizadas no mesmo processador.
Por simplicidade, assumimos que os custos da comunicação inter-processadores e intra-processador são comparáveis. Talvez surpreendentemente, esta assunção não é descabida em muitos multi-computadores, a menos que a comunicação intra-processador esteja altamente optimizada.
Isto, porque os custos de copia memória-para-memória e a comutação de contextos efectuada num implementação típica de comunicação intra-processador é muitas vezes comparável com o custo da comunicação inter-processadores.
Noutros ambientes, tais como estações de trabalho ligadas por Ethernet, a comunicação intra-processador é muito mais rápida.
Na arquitectura idealizada de um multi-computador, o custo do envio de uma mensagem entre duas tarefas localizadas em diferentes processadores pode ser representado por dois parâmetros:
o tempo de estabelecimento da mensagem, , que é o tempo necessário para iniciar a comunicação, e o tempo de transferência por palavra (tipicamente 4-octetos),
, determinado pela largura de banda física do canal de comunicação que liga os processadores origem e destino.
Tal como é ilustrado pela figura 3.3, o tempo necessário para enviar uma mensagem de tamanho
palavras é então
Este modelo idealizado de rendimento da comunicação é apropriado para muitos casos mas não cobre todas as situações. Modelos mais detalhados são apresentados na secção 3.7.
A tabela 3.1 lista os valores aproximados de e
para alguns computadores paralelos: Porque esses valores tendem a mudar rapidamente à medida que os equipamentos e o software evoluem, estes devem ser verificados antes de serem usados em modelos de rendimento. É de salientar a variação considerável tanto nos valores de
como de
. Nitidamente, computadores diferentes têm características de rendimento de comunicação muito diferentes .
Os valores na tabela 3.1 obtidos, quer da literatura quer por ajuste da equação 3.1 aos tempos de comunicação, foram medidos através de um pequeno programa de teste que envia mensagens para a frente e para trás entre dois processadores.
A figura 3.4 apresenta alguns valores experimentais representativos de dados obtidos com este programa. Estes tempos são para uma viagem simples de ida e de volta, pelo que são duplos dos obtidos pela equação 3.1. O impacto dos custos de estabelecimento e de transmissão por palavra nos tempos de comunicação é claramente visível. São de realçar as irregularidades tanto em Ethernet como em FDDI para pequenas mensagens e os saltos periódicos nos tempos do Paragom. Estes são devidos às especificidades dos protocolos de comunicação e das estratégias de gestão dos tampões de memória usados pelas bibliotecas de comunicação. Apesar de tudo, vemos que a equação 3.1 é uma representação razoavelmente precisa dos custos de comunicação, particularmente no caso de mensagens grandes.
Os valores do quadro da tabela 3.1 representam o ``melhor rendimento alcançavel'' e podem em geral ser usados como limites inferiores dos custos de comunicação quando se estima o rendimento. Aplicações com padrões de comunicação menos regulares ou menos estruturados podem ter rendimentos inferiores. Notar, também, que os valores do quadro da tabela 3.1 não incluem outros custos, tais como, a gestão dos tampões de memória associados à passagem de mensagens.
Contudo, esses custos são normalmente proporcionais ao número e tamanho das mensagens transmitidas. Assim, é, em geral, possível, através do ajuste da equação 3.1 aos dados empíricos, obter valores para e para
dependentes do sistema e do algoritmo, para os quais a equação 3.1 é válida, para um largo espectro de problemas e de tamanhos de máquinas. Este procedimento é aplicado, mais adiante, em vários exemplos neste capítulo.
Tempo de ócio
Tanto os tempos de comunicação como os de computação são especificados explicitamente num algoritmo paralelo; daí que é geralmente imediato determinar as suas contribuições para o tempo total de execução.
Contudo, o tempo de ócio (
) pode ser mais difícil de determinar, uma vez que depende muitas vezes da ordem pela qual as operações são efectuadas.
Um processador pode estar ocioso devido à falta de computação ou falta de dados.
No primeiro caso, o tempo de ócio pode ser evitado usando técnicas de balanceamento de carga, tais como as introduzidas na secção 2.5.1.
No segundo caso, o processador está ocioso enquanto a computação e a comunicação necessárias para gerar dados remotos estão a ser efectuados. Este tempo de ócio pode algumas vezes ser evitado estruturando o programa de forma a que os processadores efectuem outros cálculos ou comunicações, enquanto esperam pelos dados remotos.
Esta técnica, é designada por sobreposição de computação e comunicação, uma vez que a computação local é efectuada concorrentemente com a comunicação e a computação remotas (figura 3.5). Tal sobreposição pode ser alcançada de duas formas. Uma abordagem simples é criar múltiplas tarefas em cada processador. Quando uma tarefa bloqueia à espera dos dados remotos, o processador pode comutar para uma outra tarefa para qual já há dados disponíveis. Esta abordagem tem a vantagem da simplicidade mas só é eficiente se o custo do escalonamento de uma nova tarefa for inferior ao tempo de ócio que se procura evitar. Em alternativa, uma tarefa simples pode ser estruturada de forma que os pedido de dados remotos sejam entremeados explicitamente com outra computação.
---------------------------------------------------
Exemplo 3.1
(Diferenças Finitas)
Neste capítulo, usamos um algoritmo paralelo de diferenças finitas semelhante ao modelo de atmosfera considerado na secção 2.6, para ilustrar como os modelos de rendimento são desenvolvidos e usados.
Por simplicidade, assumimos uma malha de tamanho
pontos, sendo
o número de pontos na dimensão vertical. Inicialmente assumimos que a malha é decomposta ao longo da dimensão horizontal e repartida entre
tarefas, com cada tarefa responsável por uma sub-malha de tamanho
pontos. Cada tarefa efectua os mesmos cálculos em cada ponto e em cada iteração. Porque o algoritmo paralelo não replica a computação, podemos modelar o tempo de computação em cada iteração como
Tal como na secção 2.6 consideramos um stencil de 9 pontos, o que significa que cada tarefa deve trocar pontos de dados com duas tarefas vizinhas, para um total de duas mensagens e
dados. (Assumimos que a cada processador está alocado uma malha com pelo menos
pontos; caso contrário, são necessárias comunicações com mais do que dois vizinhos. Daí, o modelo de rendimento que desenvolvemos não é aplicável a mais do que
processadores.) O custo total das comunicações, acumulado através dos
processadores é
O tempo de execução nem sempre é a métrica mais conveniente de avaliação do rendimento de um algoritmo paralelo.
Como o tempo de execução tende a variar com o tamanho do problema, os tempos de execução têm de ser normalizados para comparação do rendimento do algoritmo para diferentes tamanhos de um problema.
A eficiência - a fracção de tempo que os processadores gastam a fazer trabalho útil - é uma métrica relacionada que pode, muitas vezes, fornecer uma medida mais conveniente da qualidade de um algoritmo paralelo.
Esta, caracteriza o uso efectivo pelo algoritmo dos recursos de um computador paralelo de uma maneira independente do tamanho do problema.
Definimos a eficiência relativa como
As quantidades definidas pelas equações 3.5 e 3.6 são chamadas eficiência e ganho relativas porque estas são definidas com respeito à execução do algoritmo paralelo num único processador.
Estas são úteis quando se explora a escalabilidade de um algoritmo mas não constituem um valor de mérito absoluto.
Por exemplo, se assumirmos que um algoritmo paralelo demora segundos, em
processador, e
segundos em
processadores.
Um outro algoritmo demora
segundos em
processador e
segundos em
processadores. Claramente, o segundo algoritmo é superior, para
entre
e
. Apesar de atingir um ganho de apenas
quando comparado com o ganho de
para o primeiro algoritmo.
Quando se comparam dois algoritmos pode ser útil ter um métrica diferente do tempo de execução, independente do algoritmo.
Por isso, definimos a eficiência absoluta e o ganho absoluto usando como base o tempo de um uni-processador para o melhor dos algoritmos conhecidos.
Deste ponto em diante, usaremos frequentemente os termos eficiência e ganho sem os qualificar como relativos ou absolutos. Contudo, o contexto tornará sempre claro o significado a atribuir.
---------------------------------------------------
Exemplo 3.2 (Eficiência do algoritmo das diferenças finitas)
No algoritmo das diferenças finitas,
e, assim, da equação 3.4 temos o seguinte modelo para a eficiência na ausência de desajustes de carga e quando
divide
:
É importante recordar que os modelos de rendimento são abstracções de problemas bem mais complexos. A partir do momento em que se concretiza o algoritmo, estamos em condições de validar os modelos e assim aumentar a confiança na sua qualidade. Contudo, nas primeiras etapas do desenho do algoritmo, devemos ser necessariamente cautelosos, especialmente se estivermos a fazer previsões quantitativas, ou se o computador específico tiver uma arquitectura muito diferente do modelo idealizado do multi-computador.
Um aspecto importante da análise de rendimento é o estudo da variação do rendimento de um algoritmo com parâmetros tais como o tamanho do problema, o número de processadores e o custo de arranque de uma mensagem.
Em particular, podemos avaliar a escalabilidade de um algoritmo paralelo, isto é, quão efectivamente podemos usar um número crescente de processadores.
Uma abordagem à quantificação da escalabilidade é a determinação da forma como o tempo de execução e a eficiência
variam com o aumento do número de processadores
, para um problema fixo em tamanho e parâmetros da máquina.
Esta análise de um problema fixo permite-nos responder a questões do tipo: - Quão rapidamente pode ser resolvido o problema
no computador
?, e ainda, - Qual o número máximo de processadores que podem ser usados quando se pretender manter a eficiência a 50 porcento? Esta última questão pode ter interesse se o computador for partilhado e houver encargos para cada processador usado.
É importante considerar tanto como
quando se avalia a escalabilidade.
Apesar de
, em geral, diminuir monotonamente com
, na verdade
pode crescer se o modelo de rendimento incluir um termo proporcional a uma potência positiva de
.
Nestes casos, pode não ser produtivo usar mais do que um número máximo de processadores, para um determinado tamanho de problema e parâmetros da máquina.
---------------------------------------------------
Exemplo 3.3 (Escalabilidade do Algoritmo de Diferenças Finitas)
A figura 3.6 ilustra a análise de problemas fixos aplicada ao algoritmo das diferenças finitas (equações 3.4 e 3.7). Esta figura representa graficamente
e
como uma função de
e de
, usando os parâmetros característicos de um multi-computador de grão relativamente fino. O custo de computação
seg foi obtido experimentalmente, da forma descrita no exemplo 3.5.
Recordar que, porque o algoritmo obriga a cada tarefa conter pelo menos duas colunas da grelha, podem usar-se com productividade no máximo 64 processadores quando
e
processadores quando
.
No capítulo, mais adiante, veremos como estas previsões podem ser comparadas com o rendimento obervado.
Os grandes computadores paralelos são frequentemente usados, não apenas, para resolver rapidamente problemas de tamanho fixo, mas também, para resolver problemas maiores.
Esta consideração encoraja uma abordagem diferente à análise de algoritmos, chamada análise de problemas escaláveis, em que consideramos não o modo como varia com
, mas de que forma deverá crescer a quantidade de computação com
para manter
constante.
Esta função de
é chamada função de iso-eficiência de um algoritmo e pode fornecer um conhecimento valioso sobre o comportamento do algoritmo.
Um algoritmo com uma função de iso-eficiência de
é altamente escalável, uma vez que, a quantidade de computação, só, necessita de crescer linearmente com respeito a
, para manter a eficiência
constante.
Em contraste, um algoritmo com uma função de iso-eficiência quadrática ou exponencial seria muito pobremente escalável.
Recordar que a eficiência é definida como a razão entre o tempo de execução num só processador e o tempo total de execução de todos os
processadores.
Assim, para manter constante a eficiência , para valores crescentes de
deverá ser mantida a seguinte relação:
Isto é, o tempo num uni-processador deverá crescer na mesma proporção que o tempo paralelo total, ou equivalentemente, a quantidade de computação efectiva deverá crescer na mesma proporção das sobrecargas introduzidas pela replicação da computação, pela comunicação e pelo tempo de ócio.
A análise de problemas escaláveis não faz sentido para todos os tipos de problemas.
Restrições temporais reais, por exemplo na previsão de clima, obrigam a que a computação se complete num intervalo de tempo fixo.
Noutras aplicações, não é possível escalar, por restrições físicas do tamanho do próprio problema.
Por exemplo, na modelação molecular, o número de átomos numa molécula é finito, tal como é fixo o número de pixéis em aplicações de processamento de imagens.
---------------------------------------------------
Exemplo 3.4 (Iso-eficiência dos algoritmos de diferenças finitas)
Usamos a análise da iso-eficiência para examinar a escalabilidade de dois algoritmos de diferenças finitas.
Recordar que a iso-eficiência de um algoritmo baseado numa decomposição 1-D de uma grelha
*
*
é dada pela equação 3.7. Para uma eficiência constante, a função de
quando substituída por
, deverá satisfazer a seguinte relação, para valores crescentes de
e
constante:
A função satisfaz os requisitos e dá lugar à seguinte relação, para todos os valores de
:
Este exemplo ilustra uma regra geral: A decomposição num elevado número de dimensões tende a ser mais eficiente que a decomposição num número reduzido de dimensões. Para compreender as razões, consideremos as equações 3.7 e 3.8.
Enquanto a decomposição 2-D envia um número maior, mas não significativo, de mensagens ( quatro em vez de duas), o volume de dados é reduzido de um factor de , de
para
.
Os custos totais das comunicações são reduzidos, a menos que
e
sejam pequenos ou
seja muito maior do que
.
Um rendimento pobre pode ser provocado por uma excessiva replicação da computação, pelo tempo de ócio, pelo arranque das mensagens, pelos custos de transferência de dados, ou qualquer combinação destes factores.
Um primeiro passo, importante quando se pretende melhorar um algoritmo, é a identificação de qual daqueles factores é dominante.
Podemos fazer isto, calculando para o algoritmo um perfil esperado de execução que indique a contribuição daqueles diferentes factores para o tempo de execução como uma função de e/ou de
.
Esta abordagem é ilustrada na figura 3.8 para um algoritmo 1-D de diferenças finitas.
O modelo prevê que quando este algoritmo é executado num multi-computador com apenas uma camada vertical , a transferência de dados domina o tempo de execução quando
é grande; o custo de arranque das mensagens é também significativo.
Se o número de camadas verticais aumentar, os custos de arranque das mensagens passam a ser insignificantes, melhorando a eficiência em geral.
A informação dos custos desta espécie pode ser usada para reorientar o desenho de um algoritmo. Esta, pode muitas vezes motivarmos para reconsiderar decisões tomadas cedo demais no processo de desenho. Por exemplo, se a replicação da computação provocar uma redução de rendimento, então pode ser a altura de reavaliar um algoritmo, anteriormente abandonado, que evitava a replicação da computação à custa do aumento das comunicações. Em alternativa, custos elevados de estabelecimento de mensagens podem sugerir novos agrupamentos de forma a aumentar a granularidade. De forma idêntica, se os custos de transferência de dados forem elevados, podemos procurar replicar a computação ou enviar mais mensagens curtas se, ao fazermos isto, podermos reduzir o volume total de dados transferidos.
Estudos experimentais podem ser usados nas primeiras fases de desenho para determinar os valores dos parâmetros usados nos modelos de rendimento, tais como o tempo de computação por ponto de uma malha, a profundidade média de pesquisa numa árvore, ou os custos de arranque e de transferência de mensagens. Estes valores podem também ser usados para comparar, posteriormente, o rendimento observado com os modelos de rendimento.
No que se segue são revisitados vários tópicos, subtis, que podem vir à superfície durante a realização dos estudos experimentais.
O primeiro passo a seguir num estudo experimental é a identificação dos dados que pretendemos obter.
Por exemplo, quando se calibra um modelo de rendimento podemos estar interessados em determinar o tempo de execução de uma versão sequencial da nossa aplicação, em função do tamanho do problema, para determinar .
Ou, podemos necessitar de medir o tempo de execução de um programa simples de teste de passagem de mensagens para podermos calcular
e
.
Geralmente, os experimentos são realizados para uma variedade de dados - diferentes tamanhos de problemas e/ou número de processadores. Maximizando o número de pontos obtidos reduzimos o impacto dos erros resultantes das medições individuais. Quando se usam dados empíricos para avaliar a qualidade de uma determinada realização, a variedade de pontos permite também estimar a precisão do modelo e identificar os regimes para os quais o modelo é inadequado.
O passo seguinte num estudo experimental é o desenho dos experimentos que irão ser usados para obter os dados requeridos. A questão crucial é assegurar que o experimento mede o que pretendemos medir. Por exemplo, se um programa compreende um passo inicial seguido por uma longa série de iterações e, o nosso modelo de rendimento trata apenas dos custos de uma iteração, então, é isso mesmo que é necessário medir.
O principal desafio numa experiência é a obtenção de resultados precisos e reprodutíveis. Os tempos de execução podem ser obtidos seguindo vários caminhos; saber qual o melhor irá depender, simultaneamente, dos requisitos e das facilidades disponíveis no computador a estudar. Uma abordagem imediata mas potencialmente dispendiosa é a que incorpora código no próprio programa que evoca rotinas de tempo de sistema para determinar o tempo gasto. Em princípio as evocações deverão ser realizadas em cada processador e a partir daí escolher o valor máximo. Contudo, pode muitas vezes identificar-se um processador que nem começa nem acaba significativamemente mais tarde, ou mais cedo, do que os outros e fazer as medições com base apenas neste processador. Em alternativa, pode usar-se uma ferramenta de traçados ou de perfis que obtém os dados automaticamente.
As experiências devem ser sempre repetidas para podermos verificar se os resultados são reprodutíveis. Em geral, os resultados não deverão variar acima duma pequeno quantidade - 2 ou 3 por cento é um valor muito grande se se pretende um ajuste fino dos algoritmos. Possíveis causas de variação incluem o seguinte:
O estudo da variabilidade dos resultados experimentais pode ajudar-nos a identificar as origens possíveis dos erros ou das incertezas das nossas medições. Contudo, mesmo quando os resultados são reprodutíveis, nunca teremos a certeza que estão correctos. A confiança nos resultados pode aumentar se medirmos a mesma coisa de diferentes maneiras confirmando que as medições redundantes são consistentes entre si. Por exemplo, para além de medir o tempo de um componente individual de um programa, podemos medir o tempo total para o programa.
Em alternativa, e de forma mais precisa, podemos realizar, recorrendo às medições, um ajuste de mínimos de quadrados da função.
(Há pacotes matemáticos tais como: Mathematica e Matlab que disponibilizam funções de ajuste.)
Um ajuste de mínimos de quadrado envolve uma minimização da soma dos quadrados das diferenças entre os valores observados, , e os valores da função correspondente,
:
Por exemplo, quando se ajusta a função com as observações de
para diferentes valores de
para calcular o valor de
, minimizamos:
Quando se ajustam os tempos de execução para diferentes números de processadores, o método que acabamos de descrever dá menos peso aos (presumivelmente mais curtos) tempos para um número elevado de processadores, apesar de estes serem tipicamente os valores de maior interesse. Por essa razão, pode usar-se um ajuste de mínimos quadrados ponderado, para o qual a diferença entre os valores observados e os valores da função são calculados da seguinte forma:
|
A figura 3.9 mostra o ajuste simples e ponderado da equação 3.2 para os dados da tabela 3.2. Os dois ajustes correspondem aos valores de de
seg e
seg, respectivamente.
Os tempos de execução previstos pelos dois modelos são apresentados na tabela 3.3. Tal como era de esperar, o ajuste simples é mais preciso para valores elevados de , enquanto que o ajuste ponderado é melhor para valores mais pequenos de
; ambos são suficientemente bons para efeitos práticos.
Estes resultados sugerem que o modelo de rendimento teórico,
caracteriza apropriadamente o tempo de computação do algoritmo das diferenças finitas.
|
Mesmo que se seja extremamente cauteloso no desenho e na realização das experiências, a natureza idealizada do modelo faz esperar uma discrepância entre os tempos de execução observados e os previsíveis. Quando as discrepâncias são significativas isso pode querer significar que, ou o modelo é errado, ou a realização do código inadequada. No primeiro caso, os resultados experimentais servem para determinar as deficiências do modelo; esta informação habilita-nos a de novo tomar em consideração o peso dos argumentos usados para justificar o desenho. No segundo caso, pode usar-se o modelo para identificar as áreas nas quais a implementação pode ser melhorada.
|
Quando postos perante uma diferença significativa entre os tempos previstos pelo modelo e os tempos de execução, o primeiro passo a seguir deverá ser testar tanto o modelo de rendimento como o nosso desenho experimental para poder verificar não apenas a correcção de cada um deles, mas também se estão a medir a mesma coisa.
O próximo passo deverá ser a obtenção de um perfil de execução do código paralelo (em contraste com o perfil discutido na secção 3.4.3, este perfil de execução deve ser baseado em valores medidos). O objectivo deverá ser obter uma visão mais detalhada do comportamento do programa medindo, por exemplo, o tempo gasto na iniciação, o tempo gasto nas diferentes fases da computação, o tempo total de ociosidade e os totais em número e volume de mensagens trocadas. Idealmente, os dados deverão ser obtidos de entre uma vasta gama de tamanhos de problemas e de número de processadores. Os quadros das tabelas 3.4 e 3.5 mostram valores típicos dos dados de perfis de execução, o exemplo é o da execução de um programa paralelo de química que incorpora o algoritmo da construção da matriz de Fock da secção 2.8 como código base. Estes dados foram obtidos usando instrumentos de medição inseridos manualmente no programa.
Um vez obtido um perfil de execução podemos compará-lo com o modelo de rendimento para identificar tanto as deficiências do modelo, como as deficiências da realização. Nas próximas secções identificamos vários problemas que podem ser revelados pela análise de um perfil de execução.
|
Uma determinada realização pode ser mais rápida que a prevista pelo modelo. Se este efeito se for acentuando à medida que o número de processadores aumenta, o fenómeno é designado por anomalia do ganho - o valor obervado do ganho é superior ao previsto. Por vezes, podemos encontrar um ganho maior do que linear sendo então chamada de super-linear. Situações deste tipo podem ocorrer nos seguintes casos:
Esta distribuição não balanceada da computação leva à existência de tempos de ócio, uma vez que em cada iteração os processadores com menos carga deverão terminar antes dos que têm carga superior. O tempo total de ócio é a diferença entre o tempo máximo de computação e o tempo de computação médio, multiplicado pelo número de processadores:
Muitas redes de interconexão usam menos do que fios para ligar os
processadores.
Assim, terão de incluir nodos de encaminhamento, ou comutadores, para encaminhar as mensagens do processador origem para o processador destino.
Um nodo de comutação pode bloquear ou reencaminhar mensagens quando várias mensagens requerem o acesso ao mesmo fio simultaneamente.
O número de fios que têm que ser atravessados para ir de um processador a outro é designado por distância entre esses dois processadores (a distância é igual ao número de comutadores mais uma unidade).
A máxima distância de um qualquer processador a um outro qualquer processador é designada por diâmetro da rede.
A distância entre dois processadores e o comprimento dos fios que os ligam não são normalmente factores significativos na determinação do rendimento, apesar de ser normalmente mais caro construir redes com fios muito longos. (O comprimento dos fios pode ser importante nas redes que se estendem por dezenas ou milhares de quilómetros, onde a velocidade da luz - cerca de 5
seg por quilómetro em cabo óptico - coloca um limite inferior no atraso da comunicação.)
Um factor que pode ter um impacto significativo no rendimento da comunicação e que estudaremos com alguma profundidade é a concorrência pela largura de banda.
Dois processadores podem necessitar de enviar dados pela mesma linha ao mesmo tempo.
Tipicamente, só uma mensagem pode ser transmitida em cada momento, razão pela qual a transmissão da outra mensagem tem de ser retardada.
Contudo, para muitos casos práticos é suficiente pensar que os dois processadores partilham a largura de banda disponível.
Assim, o termo correspondente ao volume de dados da equação 3.1 passa a depender de um factor que representa o número de processadores que necessitam de transmitir concorrentemente informação pelo mesmo fio.
O factor reflecte a ideia que a largura de banda efectivamente disponível para cada processador é da largura de banda real.
A equação 3.10 não toma em atenção os custos adicionais de congestionamento que podem incorrer quando as mensagens colidem e têm de ser retransmitidas. (Os investigadores na área das redes têm vindo a desenvolver técnicas sofisticadas de simulação para poder contabilizar esses custos. Contudo, a experiência tem demonstrado que a equação 3.10 é suficientemente precisa para muitos casos práticos.
O impacto da concorrência pela largura de banda é mais acentuado nos algoritmos que executam sincronamente i.e., os algoritmos em que todos os processadores enviam e recebem mensagens aproximadamente ao mesmo tempo e nos quais os processadores não podem prosseguir com a computação enquanto estão à espera de mensagens. O problema das diferenças finitas e muitos outros algoritmos SPMD possuem esta propriedade. Em algoritmos, tais como os de pesquisa e na construção da matriz de Fock descritos no capítulo 2, os processadores executam assincronamente estando menos sujeitos a ter de competir pela largura de banda.
O valor de na equação 3.10 pode depender quer das propriedades do algoritmo paralelo quer da rede de interconexão subjacente.
Na discussão que se segue usamos dois exemplos para ilustrar como os padrões de comunicação dum algoritmo particular podem ser analisados para determinar um valor aproximado para
em diferentes redes de interconexão.
Em primeiro lugar consideramos as propriedades das redes de interconexão.
Rede de Comutação por Barra Transversal
Uma comutação por barra transversal evita a competição pela largura de banda pelo uso de comutadores para ligar
entradas a
saídas (figura 3.11).
Neste caso,
.
Apesar de ser altamente não escalável este tipo de comutação é muito popular quando se pretende ligar um pequeno número de estações de trabalho, tipicamente, 20 ou menos.
Por exemplo, o comutador DEC GIGA pode ligar até 22 estações. Apesar de ser possível construir grandes comutadores de barra transversal (por exemplo, o Fujitsu VPP 500 usa um comutador de 224 x 224 para ligar 224 processadores) estes são extremamente dispendiosos.
Os barramentos são comummente utilizados em computadores paralelos de memória partilhada para comunicar pedidos de leitura e de escrita a uma memória global partilhada (figura 3.12). Em princípio o uso de memória global num computador de memória partilhada simplifica a programação paralela tornando a localidade um tópico a não considerar. Contudo, tal como foi discutido na secção 1.2.2, a maior parte dos computadores paralelos de memória partilhada usam caches numa tentativa de reduzir o tráfego no barramento; daí que a localidade continua a ser importante.
Ethernet A rede Ethernet, usada muitas vezes em LANs para ligar estações de trabalho ou computadores pessoais ao nível do departamento, é um outro exemplo de uma rede baseada em barramentos. Tal como é mostrado na tabela 3.1 uma rede Ethernet standard pode fornecer larguras de banda até 1 Mbytes por segundo. Todos os computadores ligados via Ethernet partilham um único canal de comunicação (figura 3.13). Um computador que necessite de enviar informação tem de esperar até que o canal esteja livre, para poder depois enviar as suas mensagens; se for detectada uma colisão, tem que esperar algum tempo e depois retransmiti-la. Uma vez que cada computador requer o uso exclusivo do canal para poder emitir, qualquer algoritmo que necessite de mais que um processador comunique simultaneamente, sofre uma redução efectiva da largura de banda. Assim, o termo S na equação 3.10, tal como noutras redes de barramento iguala o número de emissores simultâneos. O impacto da limitação da largura de banda imposta pela Ethernet ao rendimento é ilustrado pelos exemplos que se seguem.
A competição pela largura de banda numa rede em malha ocorre sempre que dois ou mais processadores tentam transmitir através do mesmo fio, ao mesmo tempo (figura 3.15). A análise usada para determinar para um algoritmo particular é ilustrada nos exemplos a seguir.
As muitas e interessantes propriedades dos hipercubos estão para além dos objectivos da nossa abordagem (mas ver capítulo 11).
Contudo, sublinhamos que dois processadores quando marcados como na (figura 3.16), estão ligados se e só se, a representação binária das suas marcações diferirem apenas numa única posição.
Exploramos esta propriedade quando se especificam algoritmos que usam uma estrutura de comunicação em hipercubo.
Uma outra importante característica de um hipercubo é que contém uma malha: este pode ser considerado uma malha com ligações adicionais de longa distância.
A conectividade adicional reduz o diâmetro e aumenta o número de fios disponíveis, especialmente no caso de comunicações não locais.
Uma desvantagem das ligações num hipercubo, do ponto de vista da engenharia, é ser mais complexo do que a malha.
Em particular, requer fios mais longos e em maior número, uma vez que um hipercubo de dimensão superior a três não pode ser representado num espaço tri-dimensional, pelo que, os fios apenas ligam processadores fisicamente adjacentes.
Numa rede MIN uni-direccional, todas as mensagens têm de atravessar o mesmo número de fios, desta forma o custo de envio de uma mensagem é independente da posição do processador. Com efeito, todos os processadores estão equi-distantes.
Numa rede MIN bi-direccional, o número de fios atravessados depende, de alguma forma, da posição do processador, apesar de em menor grau, do que o que se passa num malha ou num hipercubo (figura 3.18).
O facto de as mensagens enviadas a diferentes destinatários poderem necessitar de passar através do mesmo fio, significa que a MIN não está imune à competição pela largura de banda. De qualquer forma, uma rede MIN que liga processadores fornece
fios em cada nível, pelo que em princípio deve ser possível organizar as comunicações de forma a minimizar a competição.
A figura 3.19 ilustra quer o impacto que as limitações da largura de banda podem ter no rendimento mesmo no caso de um algoritmo simples de diferenças finitas quer o melhoramento da precisão do modelo de rendimento refinado. A figura mostra o rendimento medido em estações de trabalho ligadas em Ethernet de acordo com o previsto pelas equações 3.3 e 3.11. Vemos que o modelo mais sofisticado é razoavelmente preciso.
Os custos de comunicação por processador associados com este algoritmo são, na ausência de competição pela largura de banda,
O algoritmo pode, evidentemente, executar sem competição pela largura de banda num comutador por barra transversal. Talvez menos óbvio é que este pode também executar sem competição pela largura de banda num hipercubo de -processadores: A computação e a comunicação podem ser organizadas de forma a que cada um dos
processadores com os quais um processador tem de comunicar é um vizinho, numa das ligações do hipercubo.
Numa rede por barramento, só um processador pode comunicar em cada momento; daí que, como no algoritmo de diferenças finitas considerado no exemplo 3.7, assumimos
e da equação 3.10 temos
Como uma malha bi-direcccional a 1-D fornece apenas fios, vemos que o algoritmo paralelo não pode possivelmente prosseguir em menos do que
passos em vez dos
passos previstos atrás.
Com efeito, este pode prosseguir em passos apenas se podermos definir um escalonamento da comunicação que mantenha sempre todos os fios ocupados.
Assim, o modelo que segue representa o limite inferior dos custos de comunicação:
Um factor determinante do rendimento em muitos programas paralelos é o tempo requerido para transferir os dados entre a memória principal e a secundária, isto é, o tempo requerido para a entrada/saída (E/S). As aplicações com requisitos substanciais de E/S de dados, incluem o seguinte:
É difícil fazer um discussão geral da E/S paralela porque os diferentes computadores paralelos têm arquitecturas radicalmente diferentes de E/S, e também diferentes mecanismos de E/S. Contudo, podemos estabelecer vários pontos que têm uma extensa aplicação.
Podemos muitas vezes ganhar um conhecimento razoável do custo de uma operação de E/S pensando nela como uma comunicação entre o processador que realiza a operação e um ou mais discos. O custo de uma operação E/S de disco pode ser aproximado por um custo de arranque e um custo de transferência por palavra, de forma muita idêntica à comunicação inter-processadores. (Contudo o tempo de estabelecimento é tipicamente mais elevado.) Tal como na comunicação inter-processadores, a chave para um bom rendimento está na maximização da utilização de percursos dispoestágios e a minimização dos tempos de arranque.
Se um computador tem um único disco ou se múltiplos discos estão ligados a apenas um processador, muito pouco pode ser feito para optimizar o rendimento da E/S. Contudo, na prática, a maior parte dos computadores paralelos dispõem de múltiplos percursos dos processadores para os discos, quer através da disponibilização de múltiplos ``nodos de E/S'', quer através da ligação directa dos discos aos processadores (figura 3.22). Nas arquitecturas deste tipo, procuramos organizar as operações de E/S de maneira a que múltiplos processadores leiam e escrevam simultaneamente, usando percursos múltiplos. Assim, as estratégias centralizadas de E/S que obrigam os dados a passar por um único processador dificilmente serão eficientes e são certamente não escaláveis.
A acrescentar à maximização da concorrência das operações de E/S, necessitamos de considerar o número de pedidos de leituras e de escritas distintos, necessários para transferir os dados entre a memória dos processador e o disco. Isto pode ter muitas vezes um impacto maior no rendimento da E/S do que a quantidade de dados transferidos. O número de pedidos de E/S depende, parcialmente, da forma como os dados estão distribuídos pelos discos e pela memória. A distribuição pela memória é determinada, presumivelmente, pela aplicação; a distribuição pelo disco, tanto pode ser da responsabilidade do programador, como ser seleccionada pelo sistema de gestão de ficheiros. Os dados podem, muitas vezes, estar dispersos pelos discos dispoestágios para reduzir a probabilidade de vários processadores tentarem ter acesso ao mesmo disco simultaneamente.
Se as distribuições pelo disco e pela memória diferem, então, um elevado número de leituras e escritas podem ser necessárias para realizar a transferência de dados. Este problema é análogo ao que acontece quando se transferem estruturas de dados entre dois componentes de um programa paralelo que requerem diferentes distribuições. Tal como será discutido no capítulo 4, nesta situação são possíveis pelo menos duas abordagens: podemos modificar um ou ambos os componentes para usar diferentes distribuições, ou podemos redistribuir os dados, explicitamente, antes ou durante a sua transferência. Como os pedidos de E/S tendem a ser mais onerosos do que a comunicação inter-processadores, é muitas vezes melhor efectuar uma distribuição explícita dos dados em memória para minimizar o número de pedidos de E/S. Isto conduz a uma estratégia de acesso em duas fases, em que as distribuições de dados usadas em memória e em discos são desemparelhadas. O mérito destas várias abordagens pode ser explorado analiticamente com modelos de rendimento.
O problema da optimização de percursos envolve a procura do caminho mais curto entre todos os pares de vértices de um grafo. Um grafo compreende um conjunto
de
vértices,
e um conjunto
x
de arcos ligando vértices em
. Num grafo dirigido, cada ramo tem também uma direcção, de forma que os ramos
e
,
, são distintos.
Um grafo pode ser representado como uma matriz de adjacências
para a qual cada elemento
representa um ramo entre o elemento
e o elemento
.
se houver um ramo
; doutra forma,
(Figura 3.23).
Um percurso do vértice para o vértice
é uma sequência de ramos
,
, ...
de
para o qual nenhum dos vértices aparece mais do que uma vez.
Por exemplo,
é um percurso do vértice
para o vértice
na figura 3.23.
O percurso mais curto entre os dois vértices
e
de um grafo é o caminho que tem o número menor de ramos. O problema da optimização de percursos de origem simples requer que encontremos o percurso mais curto entre todos os pares de vértices de um grafo.
Consideramos este problema e apresentamos quatro algoritmos paralelos diferentes, dois baseados num algoritmos sequencial devido a Floyd e os outros dois baseados num algoritmo sequencial devido a Dijkstra.
Todos os quatro algoritmos tomam como entrada de dados uma matriz
de adjacências
x
e calculam uma matriz
,
x
, sendo
o comprimento do percurso mais curto de
para
, ou um valor
se não existir caminho.
procedimento floyd-sequencial
iníciose
![]()
comprimento
se exitir ramo e
![]()
senão
paraaté
![]()
paraaté
![]()
paraaté
![]()
= min(
)
arap
arap
arap![]()
fim
Esta operação de comparação é efectuada um total de vezes; por isso, podemos aproximar o custo sequencial deste algoritmo a
, sendo
o custo de uma operação simples de comparação.
paraaté
![]()
parai-inicial até i-final
paraaté
![]()
min
)
arap
arap
arap
No passo, cada tarefa requer, para além dos seus dados locais, os valores
, isto é, a coluna
de
(Figura 3.25).
Assim, especificamos que a tarefa com esta linha difunde para todas as outras tarefas. Esta comunicação pode ser efectuada usando uma estrutura em árvore em
passos.
Porque existem
difusões e cada mensagem tem tamanho
, o custo é
![]() |
(3.12) |
paraaté
![]()
parai-inicial até i-final
paraj-inicial até j-final
min
)
arap
arap
arap
Em cada um dos passos cada tarefa requer, a acrescentar aos seus dados locais,
valores de duas tarefas localizadas na mesma linha e coluna da lista bi-dimensional de tarefas (Figura 3.26). Assim, os requisitos de comunicação em cada passo
podem ser estruturados como duas operações de difusão: de cada uma das tarefas por linha que possuem parte da coluna
para todas as outras tarefas nessa linha, e de cada uma dada tarefas por coluna que possuem parte da linha
para todas as outras tarefas nesa coluna.
Em cada um dos passos,
valores devem ser difundidos para as
tarefas em cada linha e coluna, sendo o custo total
É de notar que cada tarefa serve como nodo raíz durante pelo menos uma difusão para cada tarefa na mesma linha e coluna da lista 2-D de tarefas. Os requisitos de comunicação podem ser satisfeitos lignado tarefas na mesma linha ou coluna numa estrutura em hipercubo.
O algoritmo sequencial de optimização de percursos por origem-simples de Dijkstra é dado no Algoritmo 3.2. Mantém um conjunto de vértices para o qual os caminhos mais curtos ainda não foram encontrados, sendo
o caminho mais curto conhecido de
para o vértice
.
Inicialmente,
e todos os
. Em cada passo do algoritmo, o vértice
em
com o valor mais pequeno
é removido de
.
Cada vizinho de
em
é examinado para ver se um caminho através de
pode ser mais curto do que o melhor conhecido presentemente (Figura 3.27).
Algoritmo 3.2 Algoritmo de optimização de percursos por origem-simples de Dijkstra.
procedimento dijkstra-sequencial
ínicio![]()
, para
![]()
![]()
paraaté
![]()
encontrarcom mínimo
![]()
para cada ramocom
![]()
seentão
![]()
arap![]()
arap
fim
Encontrar com
mínimo
requer em primeiro lugar uma computação local para encontrar o vértice local com mínimo e em segundo lugar uma operação de redução envolvendo todas os
tarefas no mesmo conjunto de forma a determinar o
mínimo global. A redução pode ser obtida usando a estrutura de comunicação em borboleta da secção 2.4.1, em
passos. Assim, uma vez que a redução é efectuada
vezes e involve dois valores, o custo total do algoritmo é
Claramente, a escolha do algoritmo de optimização de percursos para um problema particular envolve compromissos complexos entre a flexibilidade, escalabilidade, rendimento e a complexidade da implementação. Os modelos de rendimento desenvolvidos neste caso de estudo fornecem uma base para avaliação dos compromissos.
Um modelo de rendimento dá informação sobre um aspecto do desenho de um algoritmo: o seu rendimento paralelo esperado. Podemos usar esta informação, quando é combinada com estimativas de custo de realização, etc., para fazer escolhas informadas entre alternativas de desenho.
Em capítulos anteriores, centramo-nos no problema de desenvolver algoritmos paralelos eficientes para componentes individuais de programas, tais como a pesquisa e o cálculo de diferenças finitas. No entanto, os programas completos podem incorporar múltiplos algoritmos paralelos, cada um dos quais pode operar em diferentes estruturas de dados e estabelecer diferentes partições, comunicações ou estratégias de arranjo que permitam atingir uma execução eficiente.
A experiência mostra que a complexidade associada à construção de programas de grande dimensão pode ser controlada pela aplicação de técnicas de desenho modular. A ideia chave é encapsular a complexidade ou os aspectos variáveis do desenho em componentes separados, ou módulos , com interfaces bem definidos que indiquem os modos de interacção como o ambiente. Desenvolvem-se os programas completos encaixando os vários módulos ou compondo esses módulos. O desenho modular pode aumentar a fiabilidade e reduzir os custos porque torna mais fácil a construção de programas, a sua adaptação à alteração de requisitos e a reutilização de de componentes em novos programas.
O objectivo deste capítulo é introduzir alguns das questões de desenho que surgem quando se desenvolvem grandes programas paralelos.
A ideia básica de suporte ao desenho modular é organizar um sistema complexo (circuito electrónico, dispositivo mecânico, programa complexo) como um conjunto de componentes distintos que podem ser desenvolvidos separadamente que são depois ligados em conjunto. Apesar de parecer uma ideia simples, a experiência tem mostrado que a efectividade desta técnica depende de forma crítica na maneira de dividir um sistema em componentes e dos mecanismos usados para os encaixar. Os princípios que a seguir se enunciam são particularmente relevantes para a programação paralela.
Como exemplo, a realização de sistema de modelação das condições climatéricas fig. 2.3 pode assentar em módulos distintos para a atmosfera, para os oceanos, etc. Os interfaces de cada módulo podem compreender um pequeno conjunto de procedimentos que acedem dados limítrofes, fazem avançar a simulação etc. Assim, não é necessário ao utilizador familiarizar-se com a realização dos vários módulos que no seu conjunto podem perfazer centenas de procedimentos e dezenas de milhares de linhas de código.
Convém notar que não se diz que um módulo deverá conter funções que são logicamente relacionadas porque, por exemplo, elas resolvem a mesma parte de um problema. Este tipo de decomposição não facilita, normalmente a manutenção ou promove a reutilização de código.
Uma outra diferença entre programação sequencial e paralela, é que na primeira os módulos podem ser compostos de uma forma única: sequencial. A execução de um programa conduz a uma sequência de chamadas a funções definidas em módulos diferentes. Esta abordagem composição sequencial também pode ser usada em programação paralela, sendo fundamental no modelo de programação SPMD, usado em muitos programas paralelos. Contudo, precisam-se, muitas vezes, compôr os componente de um programa de outras formas (fig. 4.1). Em composição paralela, os diferentes módulos correm concorrentemente nos em conjuntos disjuntos de processadores. Esta estratégia pode enriquecer a modularidade e melhorar a escalabilidade e a localidade. Em composição concorrente os diferentes módulos executam concorrentemente nos mesmos processadores, sendo a execução de um módulo, particular, autorizada pela existência de dados. Esta forma de composição concorrente podem ambas reduzir a complexidade do desenho e permitindo sobrepôr computação e comunicação.
É possível distinguir entre sequencial, paralelo e composição concorrente porque são formas distintas de pensar nos programas e porque nem todas as ferramentas de programação paralela contemplam as três formas composicionais. As linguagens de paralelismo nos dados ( tais como HPF) tendem para contemplar apenas a composição sequencial. As bibliotecas de passagem de mensagens ( tais como o MPI) contemplam tanto a composição sequencial como a paralela mas não permitem composição concorrente. Outras linguagens e bibliotecas ( CC++ e Fortran M) contemplam as três formas de composição.
Na discussão anterior mostrou-se que a distribuição das estruturas de dados entre tarefas e processadores ( i.e. a forma como as estruturas de dados são divididas e atribuídas aos processadores) é um aspecto importante no desenho de algoritmos paralelos. Também se mostrou como conceber a partição de dados de forma a maximizar o rendimento e/ou minimizar os custos de desenvolvimento do software.
A distribuição de dados pode tornar-se um tópico mais complexo num +programa construído a partir de vários componentes. A escolha simples de uma distribuição óptima para cada componente pode resultar em diferentes módulos que usam diferentes distribuições de dados. Por exemplo, um módulo pode produzir um estrutura de dados vectorial distribuída por colunas, enquanto que outro módulo espera por uma entrada de dados distribuídos por linhas. Quando se compõem os dois módulos um dos módulos tem que ser modificado, caso contrário os dados terão de ser explicitamente redistribuídos na passagem de um módulo para o outro. As diferentes soluções podem ter características e custos de rendimento diferenciados.
Tanto a afinação do rendimento como a reutilização de programas é facilitada se os módulos forem concebidos de forma a serem neutros na distribuição de dados, isto é, se podem adaptar-se a diferentes distribuições de dados. Pode atingir-se esta neutralidade se a distribuição de uma determinada estrutura for especificada como um parâmetro de execução ou da própria estrutura. Por exemplo, os dois módulos anteriormente referidos podem ser definidos de forma a adaptarem-se com uma decomposição bidimensional arbitrária. O programa resultante pode assim usar uma decomposição em linhas ou em colunas, ou em última instância, uma decomposição bidimensional.
A concepção de um módulo como neutro na distribuição de dados não é necessariamente fácil. Nalguns casos distribuições diferentes de dados podem obrigar a diferentes.
Como um exemplo, considere-se o seguinte programa, que pode ser executado por cada uma das tarefas num programa S*MD de diferenças finitas.
while (not done) do
finite_difference(localgrid, localmax)
global_maximum(localmax, globmax)
if(globmax < threshold) done = true
enddo
Este programa é estruturado como uma composição sequencial de duas chamadas de procedimentos e uma instrução sequencial.
Em cada passo, cada tarefa começa por evocar o procedimento finite_difference para fazer avan'ar a simulação.
Esta evocação actualiza localgrid e retorna um valor estimado do erro localmax.
Seguidamente, cada tarefa evoca global_maximum para obter o valor global máximo do erro que será usado para determinar se a simulação converge.
Num computador paralelo, tanto a rotina finite_difference como a global_maximum têm de comunicar ( para transferir os dados necessários pelo stencil de diferença finita e calcular o máximo global), mas esta actividade é ocultado do resto do programa.
Este exemplo ilustra uma importante vantagem da composição sequencial e do modelo SPMD: o programa executado por cada processo tem uma leitura mais directa e limpa, e muitas técnicas de programação sequencial podem ser usadas indiferenciadamente. Por exemplo, os procedimentos finite_differences e global_maximum podem ser definidos em grelhas separadas e módulos reduzidos, cada um dos quais pode encapsular estruturas internas de dados ( e estruturas de comunicações).
Uma segunda vantagem da composição sequencial é que se módulos diferentes usarem a mesma distribuição de dados, não é necessário transferência de dados ( isto é comunicação) para o interface dos dados. Por exemplo, a estrutura ao nível superior de um sistema de modelação de clima pode ser da seguinte forma. Os procedimentos dos módulos oceano e atmosfera são evocados repetidamente de forma intervalada, como os dados gerados pelo módulo oceano sendo transferidos para o módulo atmosfera e vice-versa. A comunicação é precisa apenas dentro dos dois componentes
initialize_ocn(ocn_grid)
initialize_atm(atm_grid)
while(not done) do
ocean(atm_grid, ocn_grid)
atmosphere(ocn_grid, atm_grid, done)
enddo
Tal como se mostra neste exemplos uma biblioteca desenhad para ser usada num ambiente de programação SPMD pode utilizar um interface quase idêntico ao que é usado numa biblioteca sequencial comparável. A preocupação principal é que as rotinas da biblioteca sejam capazes de tratar com uma variedade de distribuições de dados ( distribuição neutra de dados) e que os detalhes da realização paralela tais como as estruturas de dados e as operações de comunicação sejam ocultadas do interface. A simplicidade da composição sequencial e da programação SPMD tem vindo a estimular alguns dos maiores projectos de desenvolvimento de bibliotecas. Um, exemplo, que pode ser apresentado para ilustrar a forma coma a distribuição neutra de dados é definida é o ScaLAPACK, uma versão da popular biblioteca de álgebra linear LAPACK concebida para executar em computadores paralelos escaláveis. O ScaLAPCK contempla uma vasta gama de operações em matrizes ''dense'' e ``bande'' tais como multiplicação, transposta e factorização. As rotinas operam em objectos de dados que representam matrizes bidimensionais decompostas usando uma distribuição em blocos,cíclica.
A distribuição de um vector é especificada por quatro parâmetros, ,
,
e
onde
e
são o número de processadores e
e
o tamanho do bloco em cada dimensão fig 4.2.
Em princípio, toda a rotina pode ser evocada com qualquer valor para os parâmetros, de forma a que programador possa experimentar distribuições de dados alternativas por simples modificação do parâmetros de evocação do programa.
Esta abordagem fornece um elevado nível de independência no arranjo, de forma que faz lembrar as directivas de distribuição de dados empregues na linguagem (HPF).
Na prática, certas limitações são colocadas nos valores permitidas para os parâmetros para simplificar o software.
Por exemplo, a rotina d factorização TU precisa que os blocos sejam quadrados.
Internamente, as rotinas ScaLAPACK podem incorporar múltiplos algoritmos paralelos que seleccionam entre esses algoritmos os que se baseiam na distribuição, no tamanho do problema e no tamanho da máquina.
Contudo, esses detalhes são ocultados do utilizador.
Não é todavia de estranhar que a composição sequencial tenha também limitações como técnica de programação estruturada para os programas paralelos.
A composição paralela pode ser vista como uma generalização do modelo de programação SPMD em que diferentes partes de um computador executam diferentes programas. Pode também ser pensada como um caso especial de composição concorrente em que a que as tarefas que executam concorrentemente têm de ser executadas em conjuntos disjuntos de processadores. Uma composição paralela especifica quais os componentes a executar em que parte do computador e de que forma os componentes vão trocar dados.
Em princípio, qualquer programa descrito como uma composição paralela pode ser convertido numa composição sequencial que intervala a execução dos vários componentes do programa de forma apropriada. Contudo, o uso da composição paralela pode enriquecer a escalabilidade e a localidade. Por exemplo, se dois componentes de um programa ( atmosfera e oceano ) podem executar concorrentemente, então o seu localização em conjunto disjuntos processadores aumenta a escalabilidade promovendo oportunidades adicionais para a execução paralela. Se a localidade aumenta com a granularidade então esta composição paralela pode fazer também uso mais eficiente da cache, memória e da largura de banda das comunicações, do que pode ser obtido pela composição sequencial. A composição paralela pode também reduzir os requisitos totais de memória pela redução da quantidade de código e de dados replicados em cada processador.
A composição concorrente tem vantagens e desvantagens em relação aos outros modos de composição. Um importante vantagem é que desta forma se facilita a ocultação de informação e dessa forma o desenvolvimento de programas modulares. Isto porque o interface neste modo de composição consiste inteiramente de canais que ligam os vários componentes. Os detalhes de realização internos que dizem respeito ao código, às estruturas de dados, à concorrência e à comunicação estão escondidos. Desta forma os componentes podem ser desenvolvidos separadamente mesmo quando são executados em diferentes processadores.
A composição concorrente pode também simplificar o desenho permitindo que as decisões sobre o arranjo e o escalonamento sejam prorrogadas ou até evitadas. Porque a semântica do programa especificado pela composição concorrente é independente da forma como os componentes são distribuídos pelos processadores, as decisões de arranjo podem ser prorrogadas até à fase final do desenho. No que diz respeito ao escalonamento como a execução é determinada pela disponibilização dos dados a ordem de execução não necessita de ser especificada pelo programador.
Uma desvantagem da composição concorrente, em alguns ambientes, é o custo de um modelo de execução conduzido pelos dados. Apesar de tanto os compiladores como os sistemas em tempo de execução poderem reduzir drasticamente os custos inerentes à comutação entre tarefas, estes custos podem ser significativos se as tarefas comutarem frequentemente.
aumento da computação A transferência de dados entre módulos pode precisar de computação o que irá aumentar os custos totais de computação. Menos frequentemente a junção de dois módulos pode permitir a redução dos custos de computação porque podem ser eliminadas operações que seriam comuns aos dois componentes.
redução do tempo de ócio O tempo de ócio pode ser reduzido na composição concorrente se a computação e a comunicação poder prosseguir noutros módulos que partilham o mesmo processador. aumento de comunicação A composição obriga muitas vezes a mais comunicação. em composição sequencial, pode ser necessário comunicação para redistribuir estruturas de dados pelos componentes. Em composição paralela é necessário comunicação para transferir dados entre os módulos que correm em diferentes processadores. aumento de granularidade A composição paralela tende a aumentar a granularidade da computação e da comunicação porque cada um executa dentro de um subconjunto dos processadores disponíveis. Este efeito pode aumentar o rendimento global.
Desiquilíbrio de Carga A composição paralela tende a aumentar o tempo de ócio se os recursos de computação atribuídos aos diferentes componentes não permitirem as mesmas velocidades de execução. Nesta situação um módulo irá completar uma qualquer fase de uma computação antes que um outro componente devendo manter em repouso até que o outro módulo forneça os dados necessários para que aquele continue a execução.