universidade do vale do itajaÍ centro de ciÊncias ...siaibib01.univali.br/pdf/david graminho...
TRANSCRIPT
UNIVERSIDADE DO VALE DO ITAJAÍ CENTRO DE CIÊNCIAS TECNOLÓGICAS DA TERRA E DO MAR
CURSO DE CIÊNCIA DA COMPUTAÇÃO
AVALIAÇÃO DE TÉCNICAS DE ENGENHARIA DE SOFTWARE PARA MODELAGEM DE PROGRAMAS PARALELOS EM AMBIENTES
MULTICORE
Área de Engenharia de Software
por
David GraminhoVictor
Marcello Thiry Comicholi da Costa, Dr. Orientador
São José (SC), junho de 2008
UNIVERSIDADE DO VALE DO ITAJAÍ CENTRO DE CIÊNCIAS TECNOLÓGICAS DA TERRA E DO MAR
CURSO DE CIÊNCIA DA COMPUTAÇÃO
AVALIAÇÃO DE TÉCNICAS DE ENGENHARIA DE SOFTWARE PARA MODELAGEM DE PROGRAMAS PARALELOS EM AMBIENTES
MULTICORE
Área de Engenharia de Software
por
David Graminho Victor
Relatório apresentado à Banca Examinadora do Trabalho de Conclusão do Curso de Ciência da Computação para análise e aprovação. Orientador: Marcello Thiry Comicholi da Costa, Dr
São José (SC), junho de 2008
ii
DEDICATÓRIA
Dedicado àquela que considero a responsável por toda e qualquer conquista em minha vida:
minha mãe!
iii
AGRADECIMENTOS
Agradeço ao meu orientador Marcello Thiry pelo empenho, profissionalismo e pontualidade
nos momentos que se fizeram necessários. Sem dúvida alguma, a realização desse trabalho não seria
possível sem seu apoio.
Agradeço aos meus familiares e amigos que de alguma forma me incentivaram direta e
indiretamente.
E agradeço à minha mãe, Araceli Graminho...por tudo! Não consigo me lembrar de nada que
eu não deva agradecê-la!
iv
SUMÁRIO
LISTA DE ABREVIATURAS...................................................................v LISTA DE FIGURAS................................................................................vi LISTA DE TABELAS..............................................................................vii RESUMO..................................................................................................viii ABSTRACT................................................................................................ix 1. INTRODUÇÃO ....................................................................................10 1.1 PROBLEMATIZAÇÃO................................................................................. 13 1.1.1 Formulação do Problema ............................................................................ 13 1.1.2 Solução Proposta .......................................................................................... 15 1.2 OBJETIVOS ................................................................................................... 16 1.2.1 Objetivo Geral.............................................................................................. 16 1.2.2 Objetivos Específicos.................................................................................... 16 1.3 METODOLOGIA........................................................................................... 17 1.3.1 Caracterização da metodologia ................................................................... 17 1.3.2 Metodologia de desenvolvimento................................................................. 17 1.4 ESTRUTURA DO TRABALHO ................................................................... 17
2. FUNDAMENTAÇÃO TEÓRICA ......................................................19 2.1 VANTAGENS DA ARQUITETURA MULTICORE................................... 21 2.1.1 Paralelismo implícito versus paralelismo explícito..................................... 22 2.2 A ARQUITETURA MULTICORE............................................................... 24 2.2.1 Taxonomia de computadores paralelos....................................................... 24 2.2.2 Multiprocessadores ...................................................................................... 25 2.3 DESEMPENHO DE COMPUTADORES MULTIPROCESSADOS.......... 30 2.4 MODELOS DE PROGRAMAÇÃO PARALELA........................................ 32 2.4.1 Estrutura básica de software paralelo ........................................................ 33 2.4.2 Linguagens para programação paralela ..................................................... 37 2.5 MODELAGEM DE SOFTWARE PARALELO .......................................... 38 2.5.1 UML.............................................................................................................. 39 2.5.2 Redes de Petri............................................................................................... 43
3. MODELAGEM DO ESTUDO DE CASO.........................................45 3.1 ESTUDO DE CASO ...................................................................................... 45 3.1.1 Modelagem (Paradigma seqüencial) .......................................................... 46 3.1.2 Modelagem (Paradigma paralelo) .............................................................. 49
4. AVALIAÇÃO E TRABALHOS FUTUROS.....................................58 REFERÊNCIAS BIBLIOGRÁFICAS ...................................................64
v
LISTA DE ABREVIATURAS
CFD Computational Fluid Dynamics CPU Central Processing Unit HTM Hardware Transactional Memory JVM Java Virtual Machine MIMD Multiple Instruction Multiple Data MISD Multiple Instruction Single Data NUMA NonUniform Memory Access RPC Remote Procedure Call SIMD Single Instruction Multiple Data SISD Single Instruction Single Data SMP Symmetric Multiprocessors SPMD Single Program Multiple Data STM Software Transactional Memory TCC Trabalho de Conclusão de Curso UMA Uniform Memory Access UML Unified Modeling Language UNIVALI Universidade do Vale do Itajaí
vi
LISTA DE FIGURAS
Figura 1. Transistores ....................................................................................................................11 Figura 2. Velocidade do processador e da memória .......................................................................12 Figura 3. Arquitetura interna de processador multicore da Intel .....................................................12 Figura 4. Método Java compartilhado por dois processos paralelos................................................14 Figura 5. Planos da Intel para produção de processadores multicore ..............................................19 Figura 6. Arquitetura multicore de barramento único .....................................................................28 Figura 7. Componente seqüencial e seções paralelizáveis ..............................................................31 Figura 8. Ganho perfeito em relação ao ganho real de desempenho de sistema paralelos................32 Figura 9. Classe ativa ....................................................................................................................40 Figura 10. Processos paralelos em Redes de Petri: (a) paralelização; (b) sincronização ..................44 Figura 11. Código Java para processos paralelos............................................................................50 Figura 12. Classe Task...................................................................................................................51 Figura 13. Grafo para o problema do caixeiro viajante ........................................................... .......53 Figura 14. Método executado em paralelo......................................................................................56
vii
LISTA DE TABELAS
Tabela 1. Taxonomia de Flynn ......................................................................................................25 Tabela 2. Protocolo write-invalidate ..............................................................................................30 Tabela 3. Protocolo write-update....................................................................................................... 30 Tabela 4. Aplicação de métricas (paradigma seqüencial) ...............................................................61 Tabela 5. Aplicação de métricas (paradigma paralelo) ...................................................................62
viii
RESUMO
VICTOR, David Graminho. Avaliação de técnicas de engenharia de software para modelagem de programas paralelos em ambientes multicore. São José, 2008. 55 f. Trabalho de Conclusão de Curso (Graduação em Ciência da Computação)–Centro de Ciências Tecnológicas da Terra e do Mar, Universidade do Vale do Itajaí, São José, 2008.
Com o advento da tecnologia multicore os problemas provenientes das limitações relacionadas à miniaturização, como o calor dissipado pelos componentes e a interferência, são atenuados. Além disso, outra vantagem surge com a tecnologia multicore: ao dividir o processamento em vários núcleos, há a possibilidade de executar as instruções das aplicações de forma paralela ao invés da execução serial, aumentando a escalabilidade e desempenho dessas aplicações. A arquitetura multicore possui, dentro de um único chip de processador, dois ou mais núcleos de execução e possibilita – com o software apropriado – a efetiva execução paralela de múltiplas threads. No entanto, para haver uma efetiva paralelização é necessário que o software também seja paralelizado de alguma forma. No modelo atual de desenvolvimento de software, o projetista utiliza uma abstração de alto nível do hardware no qual o software irá executar, sem se preocupar com detalhes de restrições físicas. Porém, para o modelo paralelo, os aspectos nos níveis mais baixos de abstração devem ser considerados. Novos paradigmas deverão ser buscados em diferentes linhas de pesquisa. Na Engenharia de Software, por exemplo, será necessário buscar abstrações adequadas à necessidade de otimização no nível físico. É justamente esse o foco principal do presente trabalho: avaliar técnicas atuais de modelagem de software paralelo que possibilitem sua implementação em nível adequado de abstração sem descuidar da escalabilidade e de maior aproveitamento da tecnologia multicore, visando garantir o nível de concorrência adequado. Palavras-chave: multicore, software paralelo, Engenharia de Software.
ix
ABSTRACT
With the advent of the multicore technology the problems proceeding from the limitations related to the miniaturization, as the heat wasted for the components and the interference, are attenuated. Beyond this solution having brought excellent benefits, another great advantage appears with the multicore technology: when dividing the processing in some cores, has the possibility to execute the instructions of the applications of parallel form instead of the serial execution being increased the scalability and performance of these applications. The multicore architecture inside possess of only chip of processor two or more cores of execution and makes possible - with appropriate software - the effective parallel multiple execution threads. However, to have an effective parallelization it is necessary that software also is paralleled of some form. In the current model of software development, the designer uses an abstraction of high level of the hardware in which software will go to execute, without if worrying about details of physical restrictions. However, for the parallel model, the aspects in the abstraction levels lowest must be considered. New paradigms will have to be searched in different lines of research. In the Engineering of Software, for example, it will be necessary to search adequate abstractions to the necessity of optimization in the physical level. And is exactly this the main focus of gift work. Thus, objective to evaluate current techniques of modeling of parallel software that make possible its implementation in an adequate level of abstraction without neglecting of the scalability and a bigger exploitation of the multicore technology aiming at to guarantee the adequate level of competition. Keywords: multicore, parallel software, Software Engineering.
1. INTRODUÇÃO
Há mais de três décadas o engenheiro Gordon Moore previu que a densidade1 de transistores
em um chip duplicaria a cada 18 meses. Essa taxa de crescimento é conhecida popularmente como
lei de Moore (FAZZIO, 2004). Essa lei baseou-se na verificação de Moore ao observar que a
dimensão linear do transistor se reduziria a cada ciclo de uma nova tecnologia, geralmente
impulsionada pela necessidade de obter melhores índices de desempenho. O aumento na velocidade
com que um computador executa uma operação está diretamente relacionado com a densidade de
transistores em um chip (FAZZIO, 2004). Com isso, consegue-se aumentar continuamente a
freqüência de operação dos processadores. A solução aplicada visando alcançar esse objetivo foi a
fabricação de transistores em camadas de silício cada vez menos espessas a fim de reduzir o seu
tamanho (CARDOSO, ROSA & FERNANDES, 2006).
A Figura 1 apresenta o tamanho das camadas de silícios e das portas de alguns transistores
existentes e de alguns protótipos. A fabricante de processadores Intel prevê, para o ano de 2011
aproximadamente, que a tecnologia a ser empregada possibilitará produzir transistores com drenos
de 22 nm (nanomilímetros) e portas de 10 nm (apresentado na Figura 1 como L). Esse cenário é
preocupante uma vez que, quanto menor for a largura da porta, mais próximas ficarão as regiões da
fonte e dreno do transistor. Segundo Cardoso, Rosa e Fernandes (2006) quando a largura da porta
chegar a 5 nm, fonte e dreno ficarão separadas por um trecho de silício tão pequeno que não
conseguirá isolá-los completamente, gerando uma probabilidade de 50% de que a corrente flua
mesmo quando não houver tensão aplicada à porta. Quando isso ocorre o transistor deixa de ser
confiável como dispositivo de processamento de dados. Assim, a miniaturização alcançará os
limites impostos pelas leis da física (FAZZIO, 2004).
Há ainda outro problema: a dissipação de energia. Como o espaço dentro de um processador
é limitado e os componentes exigem energia, a intensa junção desses aumenta a densidade
provocando interferências e uma produção excessiva de calor devido a dissipação de energia
causada pela corrente elétrica que circula nos transistores. Se essa energia não for rapidamente
removida do circuito e transferida para o ambiente, o chip atingirá temperaturas tão elevadas que,
literalmente, derreterá (CARDOSO, ROSA & FERNANDES, 2006).
1 Densidade é o número de transistores por unidade de área
11
Figura 1. Transistores
Fonte: adaptado de Cardoso, Rosa e Fernandes (2006) e Fazzio (2004)
Segundo Paolo Gargine (apud CARDOSO, ROSA & FERNANDES, 2006), Diretor de
Tecnologia da Intel, mesmo que se conseguisse contornar o limite da largura da porta, não haveria
como remover dele o calor com a mesma rapidez com que seria produzido. O chip se autodestruiria.
Há ainda outras limitações impostas pela arquitetura de um único núcleo como a estreita
banda de dados, que faz com que 75% do tempo do processador (em média) seja gasto esperando
por resultados dos acessos à memória devido à grande diferença entre a velocidade do processador e
a da memória (CARDOSO, ROSA & FERNANDES, 2006). A Figura 2 ilustra essa diferença (gap)
de velocidade. Por último, existe o custo financeiro relacionado aos processos de miniaturização.
No contexto apresentado, pode ocorrer o aumento do preço do transistor e não a diminuição, como
acontece atualmente. Em síntese, dentro do paradigma da atual arquitetura, os chips em breve não
mais evoluirão e a lei de Moore será menos relevante (FAZZIO, 2004)
12
Figura 2. Velocidade do processador e da memória
Fonte: MA (2006)
O cenário apresentado impõe limites para a miniaturização e para o número de componentes
em um chip. A tecnologia multicore é uma resposta aos problemas causados pela miniaturização
dos componentes no núcleo de um processador. Sinteticamente, a arquitetura multicore consiste em
agregar dentro de um único chip de processador dois ou mais núcleos de execução, possibilitando –
com o software apropriado – a efetiva execução paralela de múltiplas threads. O sistema
operacional trata cada um desses núcleos de execução como processadores diferentes, com todos os
seus recursos de execução associados. Cada núcleo de execução possui seu próprio cache e pode
processar várias instruções simultaneamente. A Figura 3 ilustra essa arquitetura
Figura 3. Arquitetura interna de processador multicore da Intel
Com o advento da tecnologia multicore as limitações relacionadas à miniaturização, como o
calor dissipado pelos componentes e a interferência, são atenuadas. Há outra vantagem relacionada
13
à tecnologia multicore: ao dividir o processamento em vários núcleos, há a possibilidade de
executar as instruções das aplicações de forma paralela ao invés da execução serial
(TANENBAUM, 2001), aumentando a escalabilidade e desempenho dessas aplicações. Com a
adoção de dois ou mais núcleos, é possível obter o paralelismo real entre as threads de uma
aplicação (PATTERSON & HENNESSY, 2000). O conceito de escalabilidade será apresentado na
Subseção 0
No entanto, para a efetiva execução paralela de múltiplas threads é necessário que haja um
software apropriado. Um processador multicore que não possui tal software para ser executado tem
pouca ou nenhuma utilidade (TANENBAUM, 2001). Assim, para haver uma efetiva paralelização é
necessário que o software também seja paralelizado de alguma forma.
No modelo atual de desenvolvimento de software, o projetista utiliza uma abstração de alto
nível do hardware no qual o software irá executar, sem se preocupar com detalhes de restrições
físicas. Porém, para o modelo paralelo, os aspectos nos níveis mais baixos de abstração devem ser
considerados. Novos paradigmas deverão ser buscados em diferentes linhas de pesquisa. Na
Engenharia de Software, por exemplo, será necessário buscar abstrações adequadas à necessidade
de otimização no nível físico. E é justamente esse o foco principal do presente TCC (Trabalho de
Conclusão de Curso). Assim, objetiva-se avaliar técnicas atuais de modelagem de software paralelo
que possibilitem sua implementação em um nível adequado de abstração sem descuidar da
escalabilidade e de um maior aproveitamento da tecnologia multicore, visando garantir o nível de
concorrência2 adequado.
1.1 Problematização
1.1.1 Formulação do Problema
Existem vários fatores que causam a subutilização de sistemas multicore. Aparentemente, o
software é o principal obstáculo para o uso difundido de processadores paralelos (PATTERSON &
HENNESSY, 2000). Essa constatação deve-se aos problemas inerentes à programação paralela
relacionadas à comunicação e sincronização de processos. Como a arquitetura dos processadores
multicore caracteriza-se pelo compartilhamento de recursos a programação paralela para esses
ambientes requer algum mecanismo de coordenação. O exemplo da Figura 4 demonstra os
2 Concorrência e paralelismo serão considerados sinônimos no presente trabalho
14
problemas que podem ocorrer com o paralelismo. Para essa abordagem, devem-se desconsiderar
momentaneamente os aspectos relacionados à paralelização automática (ou implícita), às políticas
transacionais ou mecanismos de controle intrínsecos a uma determinada tecnologia. A intenção
nesse exemplo é demonstrar os problemas que o paralelismo pode causar quando não há qualquer
tipo de controle entre os processos paralelos de uma aplicação.
O exemplo da Figura 4 contém um método em Java denominado saque. Esse método é
responsável por deferir o saque baseado no saldo disponível e na quantia a ser sacada. Se houver
saldo, o saque é autorizado. Caso contrário, o saque é indeferido.
Figura 4. Método Java compartilhado por dois processos paralelos.
Considera-se, para o exemplo, um saldo inicial de R$1.000,00 e dois processos executando
em paralelo, denominados processos A e B. Ao iniciar a execução da aplicação, o seguinte
cenário pode ocorrer:
• Processo A realiza uma chamada ao método saque, passando como parâmetro o valor
700, representando a quantia a ser sacada;
• Simultaneamente, o processo B realiza uma outra chamada ao método saque passando
como parâmetro o valor 500, representando a quantia a ser sacada;
• O processo A verifica na linha 10 que saldo é maior que quantia e continua sua
execução;
15
• Por se tratar de tarefas paralelas, é possível que o processo B verifique a condição da
linha 10 momentos antes do processo A efetuar a operação de subtração da linha 11.
Como ainda não foi efetuada a operação de subtração, a aplicação interpreta como
verdadeira a condição e permite que o processo B continue sua execução. Nesse ponto,
há um erro de controle;
• O processo A efetua a subtração. Agora, saldo possui R$300,00; e
• O processo B também efetua a subtração. Agora, saldo possui R$200,00 negativos.
Nesse ponto o erro se consolidou.
Esse tipo de problema, natural ao paralelismo, admite a interferência entre processos
paralelos que acessem recursos compartilhados, podendo causar a inconsistência de dados. Portanto,
os problemas inerentes ao paralelismo devem ser tratados.
1.1.2 Solução Proposta
Há duas abordagens para o paralelismo: implícito e explícito. Ambas, à sua maneira, dispõe
de mecanismos que visam resolver o problema citado na Subseção 1.1.1 . O paralelismo implícito
abstrai os problemas inerentes à programação paralela, ou seja, toda a paralelização ocorre de forma
automática e transparente para o desenvolvedor de software. É possível que a responsabilidade
sobre tal paralelização fique a cargo de um compilador que receba como entrada um programa
seqüencial escrito em uma predeterminada linguagem e produza um programa paralelo eficiente. O
grande problema nessa abordagem resume-se no grau de dificuldade em construir tal compilador
(GRUNE et al., 2001).
A abordagem alternativa ao paralelismo implícito é o paralelismo explícito. Nesse caso, a
paralelização do software – e os problemas inerentes à programação paralela - fica a cargo do
desenvolvedor. Por esse motivo, geralmente, essa abordagem é menos desejada que a anterior
(paralelismo implícito). É possível tratar os problemas inerentes à programação paralela, como
aqueles relacionados à sincronização entre processos, por meio de primitivas de sincronização. Há,
inclusive, mecanismos que se propõe a facilitar a programação paralela abstraindo a própria
sincronização entre os processos por meio de memórias transacionais como o STM (Software
Transactional Memory) ou HTM (Hardware Transactional Memory) (ADL-TABATAI,
KOZYRAKIS & SAHA, 2006). De qualquer forma, como a responsabilidade é do desenvolvedor
de software decidir parcialmente ou totalmente as questões referentes ao paralelismo, a fase de
16
modelagem do software deve contemplar os aspectos paralelos. Existem técnicas de modelagem
com suporte ao paralelismo em um nível relativamente alto de abstração. Um exemplo é a UML,
com a qual é possível modelar processos paralelos abstraindo parcialmente os aspectos dos níveis
mais baixos da arquitetura de computadores.
O presente TCC se propôs a avaliar técnicas atuais de Engenharia de Software por meio da
identificação e aplicação dessas técnicas para a modelagem de softwares paralelos.
Especificamente, foi modelada uma aplicação onde a adoção de processadores multicore e a
conseqüente paralelização do software é justificada pela necessidade de melhores índices de
desempenho. A aplicação em questão visa tratar os aspectos paralelos que podem ser aplicados
sobre o problema do caixeiro-viajante. A modelagem considerou tratar os problemas inerentes à
programação paralela sem descuidar dos requisitos da aplicação. Para a avaliação propriamente dita,
foram adotadas técnicas presentes na Engenharia de software capazes de mensurar
aproximadamente o esforço e tempo despendidos no processo de modelagem do estudo de caso.
A proposta desse trabalho visa contribuir com o desenvolvimento de software paralelo por
meio da compilação de técnicas constantes na Engenharia de Software que contemplem o
paralelismo. Porém, é válido ressaltar que o presente TCC não se propõe a desenvolver uma nova
metodologia para modelagem de softwares paralelos tampouco qualificar o desempenho entre
programas seqüenciais e paralelos.
Multicomputadores, clusters e compiladores de paralelização automática estão fora do
escopo desse trabalho. O presente TCC restringe-se ao processamento paralelo baseado na
tecnologia multicore.
1.2 Objetivos
1.2.1 Objetivo Geral
Avaliar técnicas atuais de Engenharia de Software para a modelagem de programas
paralelos em ambientes multicore.
1.2.2 Objetivos Específicos
1. Identificar técnicas atuais da Engenharia de Software que contemplem a concorrência;
2. Analisar as técnicas identificadas no item 1 a fim de selecionar aquelas que atendam aos
requisitos de natureza paralela de uma aplicação;
17
3. Aplicar as técnicas selecionadas no item 2 visando implementar os aspectos paralelos de
uma aplicação; e
4. Avaliar os resultados obtidos baseando-se em métricas estruturais orientada a objetos.
1.3 Metodologia
1.3.1 Caracterização da metodologia
O presente trabalho é de natureza aplicada, pois a proposta consiste em avaliar as técnicas de
Engenharia de Software para a modelagem de programas paralelos em ambientes multicore pela
identificação, análise e aplicação daquelas técnicas em um software específico. A abordagem será
qualitativa, uma vez que o trabalho culmina em uma avaliação sem utilizar técnicas estatísticas. Do
ponto de vista dos objetivos, a pesquisa será basicamente exploratória, pois a avaliação envolvida
possibilita a construção de hipóteses. Vinculado aos objetivos, verifica-se que os procedimentos
técnicos serão baseados principalmente em pesquisa bibliográfica, consistindo inclusive de material
obtido na internet.
1.3.2 Metodologia de desenvolvimento
O TCC é dividido em 3 fases: fundamentação teórica, modelagem, implementação e
avaliação. O TCC I contempla totalmente a fase de fundamentação teórica e parcialmente a fase de
modelagem e implementação, deixando a cargo do TCC II o restante. Assim, neste trabalho houve a
análise da arquitetura multicore, dos modelos de programação paralela e das técnicas para
modelagem de softwares paralelos por meio de pesquisa bibliográfica. Também foi determinado um
estudo de caso onde adoção de processadores multicore e a conseqüente paralelização do software
seja justificada pela necessidade de melhores índices de desempenho.
1.4 Estrutura do trabalho
Este documento está estruturado em quatro capítulos. O Capítulo 1, Introdução, apresenta
um breve histórico sobre os motivos que culminaram no desenvolvimento de processadores
multicore. Foram abordados os impactos provocados por tais processadores no desenvolvimento de
software paralelo. No Capítulo 2, Fundamentação Teórica, é apresentada a arquitetura dos
processadores multicore, as vantagens que esses processadores proporcionam e um comparativo
18
entre o paralelismo implícito e o paralelismo explícito. Serão apresentados ainda os impactos que a
tecnologia multicore e a programação paralela provocam no desenvolvimento de software e as
técnicas de Engenharia de Software para a modelagem de software paralelo. O Capítulo 3 apresenta
o estudo de caso a ser modelado segundo as técnicas de modelagem para software paralelo
identificadas no decorrer desse trabalho. Concluindo, o Capítulo 4 apresenta as considerações finais
a respeito da avaliação das técnicas de Engenharia de Software para a modelagem de softwares
paralelos em ambientes multicore.
19
2. FUNDAMENTAÇÃO TEÓRICA
Caso as previsões dos fabricantes de processadores do mercado tornem-se realidade, os
processadores multicore provavelmente serão a plataforma comum para servidores, dispositivos
móveis e desktops. Assim, os processadores multicore se tornarão o padrão industrial e não a
exceção (SHIEL, 2006). Conforme ilustrado na Figura 5. Planos, os planos da fabricante de
processadores Intel consistem em aumentar gradativamente a produção de seus processadores
multicore até o ponto em que esses se tornem a maioria disponível no mercado. Essa tendência pode
provocar uma demanda por softwares paralelos além de investimentos em processos e metodologias
para desenvolvimento.
Figura 5. Planos da Intel para produção de processadores multicore
Fonte: Intel (2005)
A opção por desenvolver software paralelo depende das necessidades específicas de cada
aplicação. Se o desempenho for elemento crítico, despender esforços para desenvolver software
paralelo pode ser uma solução viável ao considerar os possíveis ganhos que o desempenho venha
20
agregar à aplicação. Sistemas de tempo real além das aplicações que demandam complexas
simulações 3D, bancos de dados maiores, interfaces de usuário mais sofisticadas e níveis adicionais
de segurança são exemplos onde o desempenho é crítico e requerem maior poder de processamento
(CARDOSO, ROSA & FERNANDES, 2006). Para esses, os ganhos conseguidos por meio dos
sistemas multicore e a conseqüente paralelização do software podem ser significativos. Assim, a
adoção do hardware paralelo está diretamente ligada à necessidade em prover mais recursos
computacionais de forma sustentável para as aplicações. Segundo Shiel (2006) há dois motivos
relacionados à adoção de hardware capaz de executar software em paralelo:
• pela necessidade de uma aplicação executar mais rapidamente determinado conjunto de
dados: múltiplos processadores permitem a execução de uma mesma tarefa por vários
processadores simultaneamente. Esse é o conceito generalizado de processamento
paralelo. O ganho de desempenho depende da organização dos processadores, da
linguagem de programação utilizada e do grau de paralelismo possível na aplicação
(MACHADO & MAIA, 2002). O processamento paralelo e os modelos associados são
abordados na Seção 0.
• pela necessidade de uma aplicação suportar mais usuários finais ou maior conjunto de
dados: múltiplos processadores permitem a execução simultânea de diversas tarefas
independentes, aumentando o throughput3 do sistema. Servidores de banco de dados e
servidores web são exemplos desses ambientes, onde o aumento no número de
processadores permite atender número maior de usuários simultaneamente (MACHADO
& MAIA, 2002)
Para as duas abordagens, há duas possíveis soluções: (i) incrementar o poder de processamento dos
recursos que compõe um sistema e/ou (ii) adicionar mais recursos ao sistema. A primeira solução
consiste em redimensionar um sistema verticalmente, como por exemplo, substituindo um
determinado processador de 1Ghz por outro com pinagem compatível de 2Ghz. A segunda solução
consiste em redimensionar o sistema horizontalmente adicionando uma nova unidade de
processamento, a exemplo de como ocorre nos processadores multicore.
21
2.1 Vantagens da arquitetura multicore
Machado e Maia (2002) descrevem vários benefícios provenientes dos sistemas com
múltiplos processadores.
• desempenho: a princípio, sempre que novos processadores são adicionados à arquitetura
de uma máquina, melhor é o desempenho do sistema. No entanto, o ganho real de
desempenho não é linear ao número de processadores adicionados. Essa questão é
abordada com mais detalhes na Seção 0;
• escalabilidade: é a proporção entre o aumento de desempenho de um sistema com o
acréscimo de hardware e a capacidade acrescida. Ou seja, um sistema ao qual se pode
adicionar mais processadores e aumentar de modo correspondente a sua potência
computacional é dito escalável (TANENBAUM, 2001). Ao ampliar a capacidade de
computação apenas adicionando-se novos processadores é possível reduzir os custos em
relação à aquisição de um outro sistema com maior desempenho;
• relação custo/desempenho: sistemas com múltiplos processadores permitem utilizar
CPUs convencionais de baixo custo, interligadas às unidades funcionais por meio de
mecanismos de interconexão. Dessa forma, é possível oferecer sistemas de alto
desempenho com custo aceitável;
• tolerância a falhas e disponibilidade: a tolerância a falhas é a capacidade de manter o
sistema em operação mesmo em casos de falhas de algum componente. Nessa situação,
mesmo se um dos processadores falhar, os demais podem assumir suas funções de
maneira transparente aos usuários e suas aplicações, embora com menos capacidade
computacional. A disponibilidade é medida em número de minutos por ano que o
sistema permanece em funcionamento de forma ininterrupta, incluindo possíveis falhas
de hardware ou software, manutenções preventivas e corretivas; e
• balanceamento de carga: é a distribuição do processamento entre os diversos
componentes da configuração do sistema, mantendo todos os processadores ocupados o
maior tempo possível. As tarefas são movidas dos processadores sobrecarregados para
3 Throughput pode ser entendido como a quantidade de dados processados em determinado espaço de tempo
22
processadores com menor carga de processamento. Servidores de banco de dados que
oferecem esse tipo de facilidade, permitem que as solicitações dos diversos usuários
sejam distribuídas entre os vários processadores disponíveis.
Apesar das vantagens, os sistemas com múltiplos processadores também apresentam, dadas
suas características, desvantagens. As principais são aquelas relacionadas aos problemas de
comunicação e sincronização entre os processadores que estejam acessando as mesmas posições de
memória. Nesse ponto, há um fator crítico a ser considerado: as soluções para os problemas
causados pela concorrência podem ser abordados de forma implícita ou explícita. A Subseção 0
aborda com mais detalhes essa questão.
2.1.1 Paralelismo implícito versus paralelismo explícito
A programação paralela geralmente consiste em dividir o problema a ser processado em
tarefas, alocando e sincronizando essas tarefas nos processadores. Há duas abordagens para essa
atividade:
• paralelismo implícito, onde o sistema (compiladores de paralelização automática ou
outro programa capaz de realizar algum tipo de paralelização automática) particiona o
programa e aloca tarefas aos processadores automaticamente; e
• paralelismo explícito, onde o particionamento do programa em tarefas e conseqüente
alocação de recursos fica sob o controle do programador.
Paralelismo implícito
Para Grune et al. (2001), o ápice da paralelização seria a construção de um compilador que
receba como entrada um programa seqüencial escrito em uma predeterminada linguagem e produza
um programa paralelo eficiente. O multiprocessamento seria transparente aos analistas, ficando sob
sua responsabilidade apenas inserir no projeto os requisitos necessários ao processamento paralelo.
O problema nessa abordagem é saber se esse compilador pode ser implementado. Segundo Grune et
al. (2001), ainda não existe compilador desse tipo e é difícil afirmar se alguém conseguirá criá-lo,
apesar de haver um progresso substancial com a paralelização automática.
Há ainda a possibilidade de delegar ao sistema operacional funções de paralelização
automática. No entanto, uma tendência recente é mover funcionalidades do sistema operacional
para o ambiente de tempo de execução. As vantagens para essa abordagem constituem-se em
23
poupar interações dispendiosas com o sistema operacional e delegar ao desenvolvedor do
compilador um controle maior sobre a implementação das funcionalidades. Percebe-se aqui que,
geralmente, o limite entre o ambiente de tempo de execução e o sistema operacional é vago. Assim,
há de se analisar cuidadosamente onde exatamente ocorrerá o paralelismo nessa abordagem.
Tanenbaum (2003) cita que há várias organizações possíveis ao abordar sistemas
operacionais para multiprocessadores. Atualmente, o modelo SMP (Symmetric Multiprocessor –
Multiprocessador Simétrico) é amplamente utilizado pelos multiprocessadores modernos. Nesse
modelo, existe uma cópia do sistema operacional na memória sendo que qualquer CPU pode
executá-la. Assim, quando uma chamada ao sistema é efetuada, a CPU local chamada é desviada
para o núcleo e a processa. No entanto, mesmo nesse modelo, há problemas críticos relacionados ao
desempenho, à gerência de regiões críticas e à sincronização dos núcleos de processamento
persistindo, portanto, as dúvidas sobre a viabilidade em delegar completamente o paralelismo ao
sistema operacional.
Paralelismo explícito
Segundo Patterson e Hennessy (2003), o paralelismo pode ocorrer no nível de thread. Ao
considerar essa afirmação, conclui-se que o papel dos analistas e desenvolvedores de software fica
mais evidente na concepção de sistemas que possuam o paralelismo como requisito, uma vez que,
geralmente, é delegado a esses a responsabilidade da gerência das threads. O grande problema
nessa abordagem é que a programação paralela não é trivial. Patterson e Hennessy (2000) citam
alguns motivos pelos quais desenvolver programas para processamento paralelo são muito mais
difíceis do que os programas seqüenciais. Entre eles está a obrigatoriedade em obter um bom
desempenho e alta eficiência dos programas que executam em um sistema multiprocessador, pois
caso contrário, é melhor utilizar um sistema monoprocessado, cuja programação é muito mais
simples. Há, no entanto, alguns avanços na área de programação paralela. Por exemplo, a fabricante
de processadores Intel publicou pesquisas que visam facilitar a programação paralela em arquitetura
multicore e a fabricante de processadores AMD criou um mecanismo denominado LWP (Light-
Weight Profiling) que permite ao software executar melhor suas funções nas plataformas multicore.
Mesmo com esses avanços, ainda assim é difícil construir abstrações de modo que
funcionem de maneira segura na presença de vários fluxos de controle (BOOCH, RUMBAUGH &
JACOBSON, 2006). Pode haver um excesso de engenharia em sua visão de processo (muitos fluxos
concorrentes e o sistema acaba falhando) ou uma engenharia escassa (uma concorrência insuficiente
não otimiza o tempo de resposta do sistema). Outro fator está relacionado ao desenvolvimento do
24
software propriamente dito. Assim, identificar na aplicação a ser desenvolvida frações passíveis de
paralelismo, além de gerenciar a comunicação e a sincronização entre os processos, são atividades
candidatas à incorporação na análise e projeto de sistemas. Os analistas e desenvolvedores que
estiverem inseridos nesse contexto podem se deparar com a necessidade de formalizar novos
processos de desenvolvimento a serem integrados a Engenharia de Software.
Um desafio é definir um nível adequado de abstração de forma que seja possível aos
projetistas e programadores desenvolverem software que atenda aos requisitos do processamento
paralelo sem comprometer significativamente a produtividade. A Seção 0 aborda a modelagem de
sistemas com mais detalhes.
2.2 A arquitetura multicore
O que torna o desenvolvimento de programas para processamento paralelo mais difícil do
que os programas seqüenciais é que o programador precisa conhecer o funcionamento do hardware.
Em um sistema monoprocessado, o programador da linguagem de alto nível abstrai questões sobre a
organização do hardware. Essa responsabilidade é delegada ao compilador. No entanto, em
máquinas paralelas é fundamental o conhecimento do hardware para que seja possível desenvolver
programas aptos a executar em máquinas com vários processadores.
2.2.1 Taxonomia de computadores paralelos
Flynn (1972 apud TANENBAUM, 2001) criou uma taxonomia para classificar os sistemas
de computadores em geral. Essa taxonomia baseia-se em dois conceitos: (i) seqüência de instruções
e (ii) seqüência de dados. Tanenbaum (2001) cita que para cada seqüência de instruções há um
registrador program counter (PC). Assim, um sistema com n processadores tem n registradores
program counter e, por conseqüência, n seqüências de instruções. A seqüência de dados é formada
por um conjunto de operandos. As seqüências de instruções e de dados são, de certa forma,
independentes, acarretando em quatro combinações possíveis dessas seqüências, conforme descrito
na
Tabela 1. Taxonomia de Flynn.
As máquinas SISD possuem uma única seqüência de instruções e de dados, não admitindo
qualquer tipo de paralelismo. Essas são as máquinas clássicas de Von Neumann. Com relação à
categoria MISD, devido as suas características naturais (várias instruções operando sobre um
conjunto único de dados), não está claro se alguma máquina existente pode ser classificada nessa
25
categoria MISD. As máquinas que pertencem à categoria SIMD têm uma única unidade de controle
que trata uma única instrução de cada vez, mas possuem várias UALs (Unidades Aritméticas
Lógicas) que podem executar essa instrução em vários conjuntos de dados simultaneamente
(TANENBAUM, 1999). As máquinas pertencentes à categoria MIMD são aquelas formadas por um
conjunto de processadores independentes onde cada processador executa diferentes instruções. A
maioria das máquinas paralelas enquadra-se nessa categoria, incluindo os processadores multicore,
sendo, portanto a categoria mais relevante para o presente TCC.
Tabela 1. Taxonomia de Flynn
Seqüência De instruções
Seqüência de dados
Nome Exemplos
1 1 SISD Máquina clássica de Von Neumann
1 Várias SIMD Computador vetorial, processador matricial
Várias 1 MISD Nenhum exemplo
Várias Várias MIMD Multiprocessador, multicomputador
Fonte: Tanenbaum (2001)
A taxonomia de Flynn pode ser estendida. Bell (1985 apud MACHADO & MAIA, 2002)
propôs uma outra maneira de subdividir as arquiteturas MIMD, considerando apenas o grau de
acoplamento da memória principal. Assim, as máquinas pertencentes à categoria MIMD podem ser
subdivididas em dois grupos: (i) multiprocessadores e (ii) multicomputadores. Multicomputadores
são máquinas que possuem seu próprio espaço de endereçamento individual e a comunicação entre
os sistemas é realizada por meio de mecanismos de troca de mensagens (MACHADO & MAIA,
2002). Por esse motivo, os sistemas MIMD inseridos nesse contexto, são classificados como
sistemas fracamente acoplados. Os processadores multicore não pertencem a esse grupo e, por esse
motivo, não será mais considerado no presente TCC. A ênfase será sobre o primeiro grupo, ou seja,
os multiprocessadores.
2.2.2 Multiprocessadores
Multiprocessadores são máquinas com memória compartilhada. A comunicação entre os
diversos processadores é realizada por intermédio de variáveis compartilhadas armazenadas nas
memórias, sendo que todos os processadores têm acesso a todos os endereços da memória
(PATTERSON & HENNESSY, 2000). Assim, os múltiplos processadores acessam toda memória
26
disponível como um espaço de endereço global e são controlados por um único sistema operacional.
Por esse motivo, os sistemas MIMD inseridos nesse contexto, são classificados como sistemas
fortemente acoplados. A maneira como a memória compartilhada é implementada em cada um
deles divide os multiprocessadores em dois grupos baseados principalmente no tempo de acesso à
memória: (i) UMA (Uniform Memory Access – Acesso Uniforme à Memória), na qual os tempos de
acesso para todas as áreas da memória são iguais (uniformes) e (ii) NUMA (NonUniform Memory
Access – Acesso Não-Uniforme à Memória), na qual os tempos de acesso para todas as áreas da
memória não são iguais (não-uniformes). Os sistemas formados por multiprocessadores de memória
compartilhada do tipo UMA também são conhecidos como sistemas SMP (Symmetric
Multiprocessors – Sistemas com Multiprocessadores Simétricos). Machado & Maia (2002) citam
que tais sistemas (SMP) são assim chamados por implementarem a simetria dos processadores, ou
seja, todos os processadores realizam as mesmas funções, em oposição aos sistemas assimétricos,
onde somente um processador, denominado mestre, pode executar serviços do sistema operacional.
Os demais processadores, denominados escravos, precisam requisitar serviços ao processador
mestre. Para Tanenbaum (1999), a denominação SMP deve-se pelo fato de que todos os
processadores têm acesso a todos os módulos de memória, sendo tratados de forma idêntica
(simétrica) pelo sistema operacional. Por idêntica, entenda-se como todos os processadores
realizando as mesmas funções. Apesar de existirem variações do modelo de classificação abordado,
o que foi apresentado é comum à maioria dos modelos sugeridos pelos autores referenciados na
bibliografia deste TCC.
Comunicação entre processadores
As unidades funcionais (processadores, memória principal e dispositivos de E/S) de um
sistema pertencente à categoria MIMD (em especial os sistemas SMP) são interligadas de duas
maneiras básicas a fim de possibilitar a comunicação entre os processadores e as demais unidades
funcionais: (i) unidades funcionais conectadas por meio de um único barramento e (ii) unidades
funcionais conectados por meio de uma rede (PATTERSON & HENNESSY, 2000). Os sistemas
SMP são geralmente interligados por meio de um único barramento. O problema dessa organização
é que somente uma unidade funcional pode utilizar o barramento em determinado instante. Isso
pode produzir um gargalo (vide Figura 6) quando várias unidades tentam acessar o barramento
simultaneamente. Outro problema relacionado ao barramento único é que, caso ocorra algum
problema no barramento, todo o sistema fica comprometido. Uma forma de reduzir a latência das
27
operações de acesso à memória é por meio do uso de caches. Nesse esquema, cada processador
possui seu cache individual, para leitura e escrita de dados. No entanto, essa solução também
introduz um problema conhecido como coerência de cache (MACHADO & MAIA, 2002). Na
medida em que processadores que operam em paralelo precisam compartilhar dados,
independentemente se for para leitura ou escrita, há a necessidade de se estabelecer uma
coordenação para tornar possíveis as operações sobre dados compartilhados (PATTERSON &
HENNESSY, 2000). Essa coordenação é conhecida como sincronização e é abordada na Subseção
0
Machado e Maia (2002) apresentam um exemplo sobre o processo de leitura e escrita de
uma variável e os problemas de sincronização relacionados. Consideram-se, para tal, dois
processadores: PA e PB.
• O processador PA lê uma variável X na memória principal e copia o conteúdo para sua
respectiva cache.
• O processador PA altera o valor de X. Nesse ponto, o valor de X na memória cache é
diferente do valor de X na memória principal. Os valores estão, portanto, inconsistentes.
• O processador PB lê a variável X na memória principal e copia o conteúdo para sua
respectiva cache. Logo, o processador PB possui em sua cache um valor de X diferente
do valor alterado pelo processador PA.
Uma solução para esse problema é conhecida como write-through, e consiste em atualizar a
memória principal sempre que um dado na cache for alterado. No entanto, essa solução degrada o
desempenho devido aos constantes acessos à memória a cada operação de escrita, provocando
tráfego no barramento. Uma alternativa é o esquema write-back que permite alterar o dado na cache
sem alterá-lo imediatamente na memória principal. Esse esquema oferece desempenho melhor que
o esquema write-through, porém, não resolve o problema de inconsistência. Há métodos de
sincronização de processadores por meio da coerência de caches e protocolos que visam manter a
coerência dos vários processadores. Esses, por sua vez, são chamados de protocolos para
manutenção da coerência de caches.
28
Figura 6. Arquitetura multicore de barramento único
Fonte: Ma (2006)
Protocolos para manutenção da coerência de caches
Segundo Patterson e Hennessy (2000), o mais popular protocolo para manutenção da cache
é conhecido como monitoramento (snooping). Nesse protocolo, todos os controladores das caches
monitoram o barramento para determinar se elas têm ou não uma cópia do bloco compartilhado, ou
seja, cada cache verifica se o endereço do dado que trafega no barramento está localmente
armazenado. Caso esse dado seja alterado por qualquer processador a técnica de snooping deve
garantir que os demais obtenham a cópia mais recente. Os protocolos de monitoramento são de dois
tipos:
• write-invalidate (invalidação na escrita): o processador, ao realizar uma operação de
escrita e antes de modificar sua própria cópia local, envia um sinal de invalidação pelo
barramento visando invalidar todas as cópias, armazenadas em outras caches, do dado a
ser escrito (PATTERSON & HENNESSY, 2000). Assim, apenas o processador que
realizou a operação de escrita possuirá uma cópia válida do dado na cache. Os demais
processadores, ao acessarem novamente o dado em suas respectivas caches, terão uma
cópia inválida e o dado deverá ser transferido da memória principal para suas caches
(cache miss) (MACHADO & MAIA, 2002). Há nesse esquema muitos leitores mas
apenas um escritor. A Tabela 2 exemplifica o protocolo write-invalidate.
29
Tabela 2. Protocolo write-invalidate
Processador Barramento Cache do PA Cache do PB Memória
PA lê X Cache miss do PA 0 - 0
PB lê X Cache miss do PB 0 0 0
PA escreve 1 em X Invalidação de X 1 - 1
PB lê X Cachê miss de PB 1 1 1
Fonte: Machado e Maia (2002)
• write-update (atualização na escrita): esse esquema, também conhecido como escrita em
broadcast, ao invés de invalidar cada bloco compartilhado, o processador escritor envia
o novo dado em broadcast pelo barramento. Assim, todas as cópias são atualizadas com
o novo valor (PATTERSON & HENNESSY, 2000). O desempenho dessa técnica é
inferior a técnica de write-invalidate, pois além do endereço do dado a ser atualizado
também deve informar, no barramento, seu novo valor. Por isso, a técnica de write-
invalidate é empregada na maioria dos sistemas SMP (MACHADO & MAIA, 2002). A
Tabela 3 exemplifica o protocolo write-update.
Tabela 3. Protocolo write-update
Processador Barramento Cache do PA Cache do PB Memória
PA lê X Cache miss do PA 0 - 0
PB lê X Cache miss do PB 0 0 0
PA escreve 1 em X Atualização de X 1 1 1
PB lê X Cachê miss de PB 1 1 1
Fonte: Machado e Maia (2002)
A maioria dos sistemas comerciais com múltiplos processadores usam caches write-back
com o protocolo write-invalidate, a fim de reduzir o tráfego no barramento e permitir que um
número maior de processadores possa ser colocado em torno desse barramento.
Segundo Patterson e Hennessy (2000), medidas recentes mostram que dados compartilhados
têm localidade espacial e temporal mais baixa que os demais tipos de dados. Portanto, as faltas no
30
acesso dominam o padrão de acesso aos dados compartilhados armazenados na cache, mesmo
considerando que eles representam de 10% a 40% do total de acessos a dados.
Muitas das soluções apresentadas enfrentam um problema em comum: o gargalo provocado
pelo barramento único. Para evitar esse gargalo, a memória principal pode ser dividida em módulos
para permitir múltiplos acessos simultâneos, podendo ser compartilhada entre as várias unidades
funcionais. As unidades funcionais podem ser conectadas entre si por meio de um barramento
cruzado comutado (crossbar switch), criando uma matriz de interconexão (MACHADO & MAIA,
2002). Segundo Tanenbaum (2003), uma das características mais relevantes de um barramento
cruzado é que ele é uma rede não-bloqueante, ou seja, a conexão solicitada por um processador
nunca é negada caso algum ponto de cruzamento ou linha já esteja ocupado. Nesse esquema, é
possível a comunicação simultânea entre diferentes unidades funcionais, ficando a cargo do
hardware e do sistema operacional a resolução dos possíveis conflitos de acesso a uma mesma
unidade. O problema em uma arquitetura de barramento cruzado, é que para cada n processadores e
n módulos de memória são necessários n² comutadores para interligar todos os pontos. Em uma
configuração onde n seja muito grande o custo tende a ser, conseqüentemente, muito alto
(MACHADO & MAIA, 2002).
2.3 Desempenho de computadores multiprocessados
A maneira mais simples de se melhorar o desempenho de um sistema é aumentando o
número de processadores disponíveis (TANENBAUM, 2001). Se a relação fosse de 1:1, ou seja, se
o uso de n processadores acarretasse em n vezes a execução mais rápida de um programa, haveria
um ganho perfeito de desempenho No entanto, o desempenho não melhora linearmente apenas
adicionando mais processadores ou mais núcleos. Assim, adicionar n núcleos de execução dentro de
um chip não significa que o desempenho de um programa executando nesses núcleos será n vezes
maior. Essa virtual impossibilidade de se obter o ganho perfeito deve-se, em parte, ao fato de que
todos os programas têm componentes seqüenciais. Esses componentes ocorrem, na maioria das
vezes, na fase de inicialização, leitura de dados ou obtenção de resultados desse mesmo programa
(TANENBAUM, 2001). A Figura 7. Componente seqüencial e seções paralelizáveis ilustra o
componente seqüencial e as seções paralelizáveis de um programa qualquer. De acordo com a
Figura 5, F é a fração de tempo executando código eminentemente seqüencial, (1 – F) a fração de
tempo executando código potencialmente paralelizável e N é o número de processadores (ou
núcleos) adicionados ao sistema.
31
Figura 7. Componente seqüencial e seções paralelizáveis
Fonte: Adaptado de Charão (2006)
A impossibilidade de se chegar ao ganho perfeito em razão do componente seqüencial é
contemplada pela lei de Amdahl. Essa lei determina o ganho real através da utilização de vários
processadores paralelos em relação ao uso de apenas um único processador. O ganho real pode ser
obtido através da Equação 1:
Equação 1
Assim, caso não existisse o componente seqüencial em dado programa, então F seria igual a 0
(zero). Após algumas simplificações, constatar-se-ia que o ganho seria diretamente proporcional ao
número de processadores (ou núcleos) adicionados ao sistema. No entanto, como o componente
seqüencial é imperativo (pelo menos atualmente) nos programas, tem-se que F > 0 e, portanto, o
ganho linear não é possível. A Figura 8 apresenta esse comportamento. Portanto, um desafio para a
programação paralela consiste em diminuir o valor de F o máximo possível.
Segundo Tanenbaum (1999), a lei de Amdahl não é o único motivo responsável pela quase
impossibilidade de se obter o ganho perfeito. As latências na comunicação, as bandas passantes
finitas e as ineficiências algorítmicas também são fatores que cooperam com a ocorrência dessa
32
impossibilidade. No caso específico dos algoritmos, convém observar que, muitas vezes, o melhor
algoritmo não pode ser facilmente paralelizado e, portanto, um algoritmo subótimo será usado para
a execução em paralelo.
Figura 8. Ganho perfeito em relação ao ganho real de desempenho de sistema paralelos
Fonte: adaptado de Amdahl (1967)
2.4 Modelos de programação paralela
Considerando as dificuldades em realizar a paralelização automática, deve-se considerar a
escolha por um modelo de programação apropriado que contemple positivamente a execução de
tarefas em paralelo. Um modelo de programação que se propõe a executar tarefas em paralelo, é
geralmente avaliado pela sua capacidade de expressão e simplicidade, visando aumentar a
produtividade de programação.
Um modelo de programação paralela consiste em uma arquitetura computacional que
forneça recursos capazes de executar código em paralelo e um software projetado para expressar o
paralelismo. Esse software pode ser um compilador, bibliotecas ou outro recurso que habilita a
aplicação a usar hardware paralelo. Modelos paralelos são implementados de várias formas: como
bibliotecas invocadas por linguagens seqüenciais, como extensão de linguagens que forneçam
parcialmente algum recurso passível de paralelização ou como uma linguagem completamente nova
capaz de efetuar todo tipo de processamento paralelo. Esses modelos são válidos tanto para os
sistemas de memória compartilhada como os de memória distribuída.
33
Tanenbaum (2001) classifica a produção de software para máquinas paralelas em quatro
metodologias: (i) acrescentando bibliotecas especiais para o tratamento de números às linguagens
de programação seqüencial; (ii) incorporando ao ambiente de execução bibliotecas especiais que
contêm primitivas de comunicação e controle; (iii) acrescentando à linguagem de programação
estruturas especiais capazes de efetuar algum tipo de processamento paralelo como, por exemplo, a
possibilidade de criar com facilidade novos processos paralelos ou executar as iterações de um loop
em paralelo e (iv) por meio do desenvolvimento de linguagens de programação novas, voltadas para
a programação paralela. Sobre essas metodologias é que se deve embasar a escolha por um modelo
de programação apropriado.
2.4.1 Estrutura básica de software paralelo
Todas as quatro metodologias apresentadas na Seção 0 possuem vantagens e desvantagens.
A escolha deve ser baseada em uma análise criteriosa sobre cinco aspectos que formam a estrutura
básica dos softwares para computadores paralelos (TANENBAUM, 2001): (i) modelos de controle;
(ii) granularidade do paralelismo; (iii) paradigmas computacionais; (iv) métodos de comunicação e
(v) primitivas de sincronização.
Modelos de controle
Há dois modelos básicos de controle que se referem ao suporte a uma ou várias linhas de
controle, também conhecidas como threads. Para o primeiro caso, o modelo com suporte a apenas
uma linha de controle, há um único programa e um único PC, porém há vários conjuntos de dados.
Na medida em que cada instrução é emitida, ela é executada simultaneamente sobre todos os
conjuntos de dados, por diferentes elementos de processamento. Para o segundo caso, ou seja, o
modelo com suporte para várias threads, cada uma delas tem seu próprio PC, seus registradores e
suas variáveis locais. Cada thread executa seu próprio programa, com seus próprios dados,
possivelmente se comunicando e sincronizando com outras threads em determinados momentos. As
variações em torno dessa idéia básica formam o modelo dominante para o processamento paralelo
(TANENBAUM, 2001).
34
Threads
Para que seja possível paralelizar o problema, é necessário que de alguma forma seja
possível explicitar o paralelismo. Há estruturas denominadas threads que possibilitam a execução
em paralelo de vários fluxos de controle. Threads são úteis em sistemas com múltiplos
processadores, para os quais o paralelismo real é possível (TANNENBAUM, 2003). As threads
podem ser de usuário ou de kernel (também conhecidas como threads nativas). O mapeamento das
threads de usuário para as threads de kernel depende diretamente da implementação de cada
sistema operacional. Dependendo do modelo adotado, cada thread criado pelo desenvolvedor será
diretamente mapeada para uma thread de kernel. Esse modelo é conhecido como “um-para-um” e é
usado por alguns sistemas operacionais como o Windows NT. Esses sistemas operacionais
executam threads de acordo com o número de núcleos disponíveis no sistema baseado na
arquitetura multicore.
O paralelismo conseguido com as threads depende diretamente do sistema operacional e do
modelo que o mesmo adota. O controle pelo escalonamento dos processadores fica a cargo do
sistema operacional e do modelo que o mesmo adota (ou seja, não é possível controlar as decisões
do agendador). Mesmo que algumas funções como “SetThreadIdealProcessor( )” possibilitem
especificar determinado processador para a execução de uma thread, isso não garante que esse
processador em questão seja escolhido no processo de escalonamento.
No caso dos processadores multicore, pelo fato dos mesmos serem homogêneos e que
qualquer núcleo disponível pode ser utilizado para executar um processo (SILBERSCHATZ, 2004),
é possível utilizar uma fila de processos separada para cada processador. No entanto, nesse esquema
um processador pode ficar ocioso (devido a sua fila de processos prontos estar vazia) enquanto que
no mesmo instante outro (núcleo) esteja sobrecarregado (com sua fila de processos prontos cheia).
Para evitar essa situação, utiliza-se uma fila de processos comum aos processadores sendo esses,
por sua vez, agendados para qualquer processador disponível. Porém, essa abordagem também
possui problemas sendo o mais evidente aqueles relacionados ao compartilhamento dos dados, pois
é possível que os processadores do sistema acessem e atualizem uma estrutura de dados comum
(SILBERSCHATZ, 2004) em determinado segmento de código conhecido como seção crítica e que
possam causar a inconsistência de dados. Para tanto, é necessário que sejam adotados métodos que
coordenem e sincronizem o acesso à seção crítica. Monitores são, dentre as soluções possíveis, uma
estrutura de sincronização de alto nível. Essa estrutura consiste em (i) declarações de variáveis
cujos valores definem o estado de uma instância desse tipo e (ii) corpos de procedimentos ou
funções que implementam operações desse tipo (SILBERSCHATZ, 2004). A estrutura do tipo
35
monitor garante que somente um processo de cada vez pode estar ativo dentro do monitor.
Algumas linguagens de programação (como Java) suportam diretamente o conceito de monitores.
Granularidade do paralelismo
O paralelismo pode ocorrer em vários níveis: do nível mais baixo, como as instruções de
máquinas, até os níveis mais altos de abstração, pelo uso de vários processos independentes na
solução de um único problema. Há vários níveis contidos dentro do intervalo entre os citados níveis
(instruções de máquina e os de vários processos). Entre esses, há aqueles que se utilizam de
processos leves como as threads. Nesse caso, o paralelismo é evidenciado quando as threads
executam de maneira independente uma das outras, possivelmente em processadores diferentes. Em
alguns sistemas, o sistema operacional controla todas as threads e é o principal responsável pelo seu
escalonamento. Em outros, cada processo de usuário é responsável por escalonar e gerenciar suas
threads, sem que o sistema operacional sequer saiba de sua existência. O paralelismo no nível de
threads é amplamente utilizado em algumas linguagens de programação como Java, que permite o
uso explícito desses recursos.
Paradigmas computacionais
Os paradigmas computacionais responsabilizam-se pela estrutura de trabalho da maioria dos
programas paralelos, em especial aqueles que suportam as threads e processos independentes. O
SPMD (Single Program Multiple Data – Único Programa Múltiplos Dados), por exemplo, executa
o mesmo programa em conjuntos de dados diferentes. Isso ocorre porque há apenas uma linha de
controle e várias unidades de execução. Há ainda outros paradigmas como, por exemplo, o pipeline.
Método de comunicação
Processos executados em paralelo precisam se comunicar. Essa comunicação pode ser feita
por variáveis compartilhadas ou por troca explícita de mensagens. Na primeira técnica, todos os
processos têm acesso a uma memória comum e se comunicam lendo e escrevendo nessa memória.
Em um sistema multiprocessador, as variáveis podem ser compartilhadas entre vários processos
mapeando a mesma página no espaço de endereçamento de cada um dos processos. Na segunda
técnica, os processos usam na comunicação primitivas como send e receive. Um processo executa
uma primitiva send, identificando outro processo como destino. Quando esse último processo
executar uma primitiva receive, a mensagem é copiada em seu espaço de endereçamento
36
(TANENBAUM, 2001). Essa técnica é comumente utilizada em multicomputadores ao passo que, a
primeira, é utilizada em sistemas multiprocessados (multicore). Multicomputadores também podem
fazer uso de variáveis compartilhadas, mas essa discussão está fora do escopo do TCC.
Primitivas de sincronização
A comunicação entre processos paralelos geralmente exige a sincronização de suas ações.
Por exemplo, quando há o compartilhamento de variáveis logicamente é necessário que haja a
garantia que, enquanto um processo estiver escrevendo na estrutura de dados compartilhada,
nenhum outro poderá ter acesso a essa estrutura. Para evitar que mais de um processo utilize os
mesmos dados compartilhados ao mesmo tempo é necessário que haja uma forma de sincronizar o
acesso a esses dados. Assim, a sincronização consiste em permitir a requisição ao uso exclusivo de
algum recurso compartilhado pela exclusão mútua entre processos. A exclusão mútua pode ser
garantida por meio de várias primitivas de comunicação como semáforos, travamentos, mutexes e
regiões críticas (TANENBAUM, 2001). Está fora do escopo deste TCC discutir os conceitos dessas
primitivas de comunicação.
As metodologias apresentadas fornecem uma base para iniciar a definição de um modelo
que contemple o desenvolvimento de software paralelo. Considerando exclusivamente a arquitetura
dos sistemas multicore e o contexto deste trabalho, define-se:
• O modelo de controle adotado será aquele com suporte a várias linhas de controle, ou
seja, com suporte a várias threads. Esses sistemas são conhecidos como multithread.
Essa escolha se baseia no fato de que as variações em torno dessa idéia formam o
modelo dominante para o processamento paralelo;
• A granularidade do paralelismo também contemplará as threads, haja vista que os
processadores multicore oferecem suporte ao paralelismo nos níveis mais baixos, como
o pipeline;
• O paradigma computacional adotado está fortemente acoplado à decisão dos itens
anteriores. Assim, o paradigma computacional adotado será aquele relacionado ao
suporte às threads e processos independentes;
• Os processadores multicore possuem memória compartilhada. Assim, o método de
comunicação adotado será aquele onde as variáveis podem ser compartilhadas entre
37
vários processos mapeando a mesma página no espaço de endereçamento de cada um
dos processos; e
• A sincronização para sistemas de memória compartilhada consiste em permitir a
requisição ao uso exclusivo de algum recurso compartilhado por meio da exclusão
mútua entre processos. A exclusão mútua pode ser garantida com várias primitivas de
comunicação.
2.4.2 Linguagens para programação paralela
Uma série de linguagens foi projetada para suportar o paralelismo, como a PL/I, ADA 95 e
Java (SEBESTA, 2000). As linguagens de programação estão vinculadas diretamente às abordagens
apresentadas na Subseção 0consistindo, portanto, em linguagens com paralelismo implícito e
linguagens com paralelismo explícito. LabVIEW, HPF, ZPL e SISAL são exemplos de algumas
implementações de linguagens com paralelismo implícito. Java, ADA e PVM são exemplos de
linguagens de programação com paralelismo explícito. Cada abordagem possui vantagens e
desvantagens. No entanto, um fator comum a essas abordagens e importante para o suporte à
concorrência é a sincronização. A sincronização visa garantir que apenas um processo poderá
acessar certa variável compartilhada em qualquer instante dado (GRUNE et al., 2001). Nos modelos
de memória compartilhada, as principais primitivas de sincronização são os semáforos e monitores.
Java e C# são linguagens baseadas no modelo de memória compartilhada com o bloqueio sendo
fornecido por monitores.
Além do suporte da sincronização, a concorrência também se caracteriza pelo modelo de
controle implementado pelas linguagens de programação. Conforme apresentado na Subseção 0o
modelo de controle dominante é aquele formado pelo suporte a várias linhas de controle, também
conhecidas como threads. A maior parte dos programas baseados em threads é feita em uma
linguagem convencional, como C, que foi estendido para ter uma biblioteca de threads. O pacote C
Threads e o padrão POSIX Threads, conhecido como pthreads, são exemplos disso. Há linguagens
que oferecem suporte direto para threads, como o Java e ADA 95 (COLOURIS, 2007).
38
Java
Java suporta o conceito de sincronização por meio da palavra-chave synchronized
garantindo que qualquer thread executando um método com aquela palavra-chave em sua assinatura
impeça outra thread de executar aquele método naquele instante.
Esse é um dos principais motivos pelo qual foi escolhido Java para a implementação do
estudo de caso. O objetivo é justificar a adoção do Java para a implementação apresentando
superficialmente o conceito de monitor devido à necessidade em utilizar esse conceito nas frações
paralelizáveis do estudo de caso. A vantagem de Java é que esta disponibiliza para o programador
as primitivas de simultaneidade além do suporte direto para as threads. O conceito de monitores foi
especialmente implementado em Java para atender também a uma necessidade da própria
linguagem de programação. Como Java roda numa máquina virtual, os programas em Java devem
compartilhar a memória destinada ao processo da máquina virtual, e assim o uso de monitores na
implementação da própria linguagem tornou-se uma facilidade que foi estendida também para a
programação.
Threads Java somente executam em sistemas que suportem diretamente o conceito de
threads e podem ser escalonadas para qualquer processador disponível. Entretanto, fica a cargo do
sistema operacional distribuir as threads pelos dos processadores.
A JVM (Java Virtual Machine) HotSpot faz o mapeamento 1:1 de todas as threads Java para
as threads nativas do sistema operacional. Esse mecanismo provê a verdadeira concorrência entre as
threads, no entanto permite erros como deadlocks em objetos sincronizados. No caso específico dos
sistemas com processadores multicore, o sistema operacional mapeia threads para diferentes
núcleos, dependendo de sua carga e disponibilidade.
2.5 Modelagem de software paralelo
Um modelo de desenvolvimento de software paralelo possui cinco aspectos (vide Subseção
0) que formam a estrutura básica dos softwares para computadores paralelos. Sinteticamente, esses
aspectos consistem na identificação de processos paralelos e na comunicação e sincronização entre
esses processos. Por conseqüência, a modelagem de software paralelo deve contemplar esses
aspectos.
Um fator sobre a modelagem e projeto de software paralelo é a possibilidade de representar
tais sistemas com formalismos existentes. Dois exemplos são a UML (Unified Modeling Language
39
– Linguagem de Modelagem Unificada) e as Redes de Petri. Com ambos, é possível representar
processos paralelos e abranger, principalmente, seus mecanismos de sincronização.
2.5.1 UML
Identificação de processos paralelos
Um modelo de controle de um determinado software paralelo pode fornecer suporte para
várias threads, onde cada thread executa seu próprio programa, com seus próprios dados,
possivelmente se comunicando e sincronizando com outras threads em determinados momentos.
Cada thread dessa é um fluxo de controle independente, capaz de iniciar uma atividade de controle.
Na UML, cada fluxo de controle independente é modelado como um objeto ativo. Portanto, um
objeto ativo é uma thread capaz de iniciar uma atividade de controle (BOOCH, RUMBAUGH &
JACOBSON, 2005). Segundo Eriksson et al. (2004), objetos ativos podem executar em paralelo
com outros objetos ativos. Um objeto ativo é uma instância de uma classe ativa sendo, portanto,
passível de representação (modelagem). Uma classe ativa representa uma unidade que executa
concorrentemente sendo implementada, geralmente, como um processo ou thread (ERIKSSON et
al., 2004). A UML fornece uma representação gráfica de uma classe ativa (vide Figura 9. Classe
ativa) constituindo-se de um retângulo com uma linha vertical extra e, como qualquer outra classe,
um compartimento para nome, atributos e operações de classe. A exceção fica por conta dos sinais,
que são enumerados em um compartimento extra. As classes ativas podem participar de
relacionamentos de dependência, generalização e associação além de poderem fazer uso de
qualquer mecanismo de extensibilidade da UML, como estereótipos, valores atribuídos e restrições.
Classes ativas podem participar em colaborações, sendo que a modelagem dessas classes é feita
com diagramas de interação (incluindo diagramas de colaboração e de seqüência). Em contraste aos
objetos ativos, existem os objetos passivos. Um objeto passivo é uma instância de uma classe
passiva. As instâncias dessa classe iniciam sua atividade somente quando outro objeto executa
alguma operação sobre as mesmas, como por exemplo, por meio do envio de uma mensagem
(ERIKSSON et al., 2004).
Classes ativas
O processo de modelagem de classes ativas geralmente consiste em identificar
oportunidades para ação paralela em um sistema. Nesse caso, eventos assíncronos, tarefas
periódicas, funções de tempo crítico e tarefas computacionais que executam em background são
40
exemplos de oportunidades passíveis de paralelização. Cada fluxo de controle é candidata a uma
classe ativa. Identificadas as classes ativas, é importante considerar uma distribuição equilibrada de
responsabilidades entre as mesmas.
Figura 9. Classe ativa
Fonte: adaptado de Booch, Rumbaugh e Jacobson (2006)
O processo de modelagem consiste ainda em examinar as demais classes ativas e passivas
com as quais cada uma colabora estaticamente. As decisões estáticas devem ser capturadas em
diagramas de classes, destacando cada classe ativa explicitamente. Com relação ao aspecto
dinâmico, deve-se assegurar que cada classe ativa (como qualquer outras classe) seja altamente
coesiva e ligeiramente acoplada em relação às classes vizinhas e que cada uma tenha o conjunto
correto de atributos, operações e sinais. Considerar como cada grupo de classes colabora com outro
dinamicamente e capturar essas decisões em diagramas de interação. Deve-se mostrar
explicitamente os objetos ativos como a raiz desses fluxos e identificar cada seqüência relacionada,
atribuindo-lhe o nome do objeto ativo.
Outro fator é generalizar conjuntos comuns de objetos ativos em uma única classe ativa
evitando assim o excesso de concorrência, visto que muitos fluxos concorrentes podem aumentar o
risco de falhas no sistema.
Comunicação entre processos
Várias estratégias podem ser utilizadas para habilitar a comunicação entre objetos ativos,
como por exemplo, por meio dos mecanismos de memória compartilhada, de envio de mensagens
ou RPC (Remote Procedure Call - Chamada Remota a Procedimento).
Objetos ativos devem estar habilitados para executar concorrentemente e enviar mensagens
para outros objetos ativos sem ter que esperar por um valor de retorno, ou seja, devem estar
habilitados para executar assincronamente (ERIKSSON et al., 2004). Segundo Booch, Rumbaugh e
Jacobson (2006), em um sistema composto por objetos passivos e ativos há quatro combinações
41
possíveis de interação: (i) uma mensagem é enviada de um objeto passivo para outro, (ii) uma
mensagem é enviada de um objeto ativo para outro, (iii) uma mensagem é enviada de um objeto
ativo para um objeto passivo e (iv) uma mensagem é enviada de um objeto passivo para um objeto
ativo. Na combinação (ii), ou seja, quando uma mensagem é enviada de um objeto ativo para outro
também ativo, há dois estilos possíveis de comunicação:
• O objeto ativo chama uma operação de outro objeto: um objeto chama a operação,
aguarda que o destinatário aceite a chamada, a operação é invocada, um objeto de
retorno é retornado ao objeto que fez a chamada, os dois objetos continuam sua
execução independentemente. Durante esse processo, os dois fluxos de controle ficam
em estado de espera; e
• Um objeto ativo assincronamente envia um sinal ou uma chamada a uma operação de
outro objeto: um objeto faz a chamada a uma operação e envia o sinal ou chama a
operação e depois continua sua execução independente. Enquanto isso, o destinatário
aceita o sinal ou chamada sempre que estiver no estado de pronto e prossegue sua
execução após concluir o que foi solicitado. Nesse caso, os objetos não estão
sincronizados.
Geralmente a comunicação entre objetos ativos é assíncrona enquanto a comunicação
interna a um objeto ativo é síncrona.
Eventos e sinais
Para Booch, Rumbaugh e Jacobson (2006), um evento é a especificação de uma ocorrência
significativa que possui uma localização no tempo e espaço. Para Eriksson et al.(2004), um evento é
algo que ocorre no sistema e que pode fornecer um gatilho (trigger) capaz de causar a execução de
determinado comportamento.
Na UML, é possível modelar quatro tipos de eventos: sinais, chamadas, contagem de tempo
e alteração no estado (BOOCH, RUMBAUGH & JACOBSON, 2006).
Um sinal é um tipo de evento que representa a especificação de um estímulo assíncrono
comunicado entre instâncias (BOOCH, RUMBAUGH & JACOBSON, 2006). É um classificador
de mensagens, ou seja, é um tipo de mensagem. Os sinais podem ter atributos e operações para
42
carregar informação e comportamento (ERIKSSON et al., 2004). Na UML, é possível modelar
sinais como classes estereotipadas.
Sincronização de processos
Sincronização é o processo de coordenar threads concorrentes para que interajam entre si de
forma eficiente (ERIKSSON et al., 2004). A coerência da cache surge como o principal exemplo
onde pode haver inconsistência de dados quando dois ou mais processadores tentam gravar valores
em uma variável compartilhada ao mesmo tempo. Esse problema é nato nas atividades que
envolvam concorrência. Qualquer descuido nesse contexto pode causar a inconsistência dos dados e
por conseqüência, provocar falha de difícil identificação. Segundo Booch, Rumbaugh e Jacobson
(2006) esse é o clássico problema da exclusão mútua. Em sistemas orientados a objetos é possível
solucionar esse problema tratando um objeto como uma região crítica que é uma primitiva de
sincronização. Existem três alternativas (mecanismos de sincronização) para essa abordagem:
• seqüencial: somente um fluxo de controle deve existir sobre o objeto por vez. Na
presença de vários fluxos de controle, a semântica e a integridade do objeto não pode ser
garantida;
• protegida: há vários fluxos de controle. A semântica e a integridade são garantidas pela
seqüencialização de todas as chamadas para todas as operações protegidas do objeto.
Dadas essas características, pode ocorrer deadlock; e
• concorrente: há vários fluxos de controle. A semântica e a integridade do objeto são
garantidas porque os fluxos de controle acessam conjuntos de dados disjuntos ou
somente dados de leitura.
Na UML, é possível anexar essas propriedades a uma operação, utilizando-se a notação de
restrição. Declarar a concorrência para uma operação significa que várias chamadas dessa operação
podem ser executadas ao mesmo tempo sem perigo. Declarar a concorrência para um objeto
significa que as chamadas de diferentes operações podem ser executadas concorrentemente sem
perigo (BOOCH, RUMBAUGH & JACOBSON, 2006). A correta modelagem de sistemas paralelos
pode evitar que os problemas inerentes a sincronização ocorram, como os acessos incorretos aos
recursos compartilhados, uso ineficiente de recursos, deadlocks e inversão de prioridade.
43
Na UML, o suporte para sincronização pode ser conseguido usando estereótipos ou
propriedades. Por exemplo, se a sincronização deve ser indicada de forma explícita, uma classe do
tipo semáforo pode ser definida e instanciada sempre que a sincronização entre objetos ativos deva
ser mostrada. Uma outra possibilidade é definir um estereótipo <<semáforo>> que pode ser usado
por todas as classes que deveriam ser protegidas pelo semáforo (ERIKSSON et al., 2004)
A sincronização deve receber a devida atenção não apenas entre objetos ativos, mas entre
objetos ativos e os objetos passivos com os quais colaboram. Os mecanismos de sincronização
(semântica de operação seqüencial, protegida ou concorrente) devem ser aplicados, conforme seja
apropriado e as propriedades de sincronização de operações devem ser mostradas explicitamente.
A UML fornece graficamente recursos como os diagramas de classes e diagramas de
interação capazes de auxiliar a visualização da forma como esses fluxos interagem uns com os
outros.
2.5.2 Redes de Petri
Redes de Petri é uma técnica de especificação de sistemas que possibilita uma representação
matemática e possui mecanismos de análise poderosos, que permitem a verificação da corretude do
sistema especificado. Apesar de não ser a linguagem adotada para o contexto do presente TCC, é
válido fazer algumas considerações a seu respeito.
Com Redes de Petri é possível modelar sistemas paralelos, concorrentes, assíncronos e não-
determinísticos. As características inerentes às Redes de Petri facilitam a modelagem de sistemas
que possuam concorrência e sincronização.
Para a modelagem, são válidas as mesmas prerrogativas apresentadas na Subseção 0, ou
seja, devem-se identificar oportunidades para ação concorrente em um sistema como eventos
assíncronos, tarefas periódicas, funções de tempo crítico e tarefas computacionais que executam em
background. Após a identificação de processos paralelos, os problemas de comunicação e
sincronização imediatamente aparecem (ERIKSSON et al., 2004) sendo possível identificá-los e
modelá-los. A representação de processos paralelos em Redes de Petri é ilustrado na Figura 10.
Processos paralelos em Redes de Petri: (a) paralelização; (b) sincronização Também é possível
modelar a comunicação e a sincronização com as Redes de Petri. A comunicação consiste em uma
simples passagem de transição enquanto que o sincronismo é representado por uma rede elementar.
44
Figura 10. Processos paralelos em Redes de Petri: (a) paralelização; (b) sincronização
A identificação de processos e dos mecanismos de comunicação e sincronização é
imperativa para o modelo de desenvolvimento de software paralelo em si, independente de
linguagens de programação e modelagem. No entanto, a abordagem sobre esses aspectos pode
variar se considerar o paradigma no qual está sendo modelado. A principal diferença entre UML e
Redes de Petri é que a primeira é comumente (mas não exclusivamente) utilizada em sistemas
orientados a objetos e a segunda em sistemas orientados a estados e ações.
45
3. MODELAGEM DO ESTUDO DE CASO
3.1 Estudo de caso
O presente TCC contempla o problema do caixeiro-viajante como estudo de caso. Esse
problema é um caso típico de otimização combinatória, freqüentemente utilizado em computação
para demonstrar problemas de difícil resolução e que pode ser representado por meio de um grafo
cujo objetivo é encontrar o ciclo hamiltoniano de menor extensão. O problema supõe que um
caixeiro-viajante deseja visitar N cidades (vértices) de certa localização e que, entre alguns pares de
cidades existem rotas (arcos ou arestas), através das quais ele pode viajar a partir de uma cidade
para outra. Cada rota tem um número associado, que pode representar a distância ou o custo
necessário para percorrê-la. Assim, o caixeiro viajante deseja encontrar um caminho que passe por
cada uma das N cidades apenas uma vez, e além disso que tenha custo menor que certo valor onde o
custo do caminho é a soma dos custos das rotas percorridas.
Uma forma de solucionar esse problema é explorar todas as permutações possíveis dos
vértices do grafo. Infelizmente esta abordagem não é prática porque não opera em tempo polinomial
pois para um conjunto de vértices V, existem V! permutações possíveis, requerendo um tempo de
O(V!), onde V! é o fatorial de V, que é o produto de todos os números de V até 1. Em geral,
algoritmos de tempo não-polinomial são evitados, porque, mesmo em pequenas operações, os
problemas rapidamente tornam-se impossíveis de serem rastreados (LOUDON, 2000).
Normalmente, o problema do caixeiro-viajante é resolvido utilizando um algoritmo de aproximação
(por exemplo, algoritmos genéticos) visando encontrar o ciclo hamiltoniano de menor extensão. No
entanto, o principal intuito do presente TCC não é necessariamente o de encontrar o caminho de
menor custo. O objetivo é utilizar sua complexidade computacional (por meio de permutações das
rotas possíveis) para avaliar a complexidade em desenvolver um software com características
paralelas baseado em ambientes multicore capazes de otimizar o desempenho computacional.
Segundo Silveira (2000), a importância do problema do caixeiro-viajante resume-se em duas
capacidades:
1. de modo simples e concreto, exemplifica a enorme velocidade de crescimento da fatorial;
2. é uma espécie de "metro" com o qual medimos a complexidade computacional dos
problemas combinatórios que ocorrem em engenharia e no trabalho científico.
46
3.1.1 Modelagem (Paradigma seqüencial)
Descrição do problema
O problema supõe que um caixeiro viajante deseja visitar N cidades (vértices) de certa
localização e que, entre alguns pares de cidades existem rotas (arcos ou arestas), através das quais
ele pode viajar a partir de uma cidade para outra passando por cada uma das N cidades apenas uma
vez .
Restrições:
1. O problema deve ser representado por meio de um grafo.
2. As questões relativas ao cálculo da aresta (rota) de menor custo não devem ser consideras.
3. Deve-se explorar (baseado nos critérios de caminhamento do algoritmo do caixeiro viajante)
todas as permutações (rotas) possíveis dos vértices do grafo.
Requisitos Funcionais 1. O sistema deve traçar todas as combinações de rotas existentes entre as coordenadas
selecionadas pelo usuário
2. O sistema deve apresentar graficamente cada rota processada
3. O sistema deve indicar o andamento do processamento
Requisitos Não-Funcionais 1. O sistema deve permitir que o processamento seja encerrado a qualquer momento pelo usuário
2. O processamento das rotas deve ocorrer em paralelo à sua apresentação gráfica
3. O processamento das rotas deve ocorrer em paralelo ao indicador de processamento
Requisitos de Interface 1. Deve existir um botão que possibilite o término do processamento pelo usuário
2. Deve existir uma barra de progresso que indique o status do processamento
Casos de uso
Nome : traça rotas
Atores : usuário
47
Descrição : usuário solicita ao sistema que gere todas as rotas existentes entre todas as
coordenadas por ele (usuário) selecionadas apresentando graficamente cada rota e um indicador de
processamento.
Fluxo principal
1. O usuário seleciona o número de coordenadas
2. O sistema inicia o processamento de todas as rotas possíveis entre as coordenadas
2.1. O sistema apresenta (traça) um gráfico para cada rota processada
2.2. O sistema atualiza o indicador de processamento a partir de cada rota processada
3. O sistema encerra o processamento
Fluxo de exceção
1. Se no passo 2 do fluxo principal o usuário decidir por encerrar o processamento do sistema,
esse (sistema) deve encerrar a apresentação do gráfico imediatamente e liberar os recursos
utilizados.
Diagrama de caso de uso
Diagrama de colaboração
49
Diagrama de seqüência
3.1.2 Modelagem (Paradigma paralelo)
A engenharia de software ocupa todos os aspectos da produção de software, desde os
estágios iniciais de especificação do sistema até a manutenção do mesmo (SOMMERVILLE, 2003).
Considerando as métricas e os objetivos a que se propõe, a avaliação do estudo de caso enfatizará os
aspectos relacionados à fase de projeto de software.
O projeto de software é formado por alguns estágios, sendo os mais evidentes o projeto de
arquitetura, especificação abstrata, projeto de interface, projeto de componentes, projeto de
estrutura de dados e projeto de algoritmos. Ao considerar o estudo de caso do presente TCC,
percebeu-se que, para os aspectos paralelos, os estágios de maior relevância (no sentido que se
diferenciam com maior evidência em relação aos aspectos seqüenciais) são o projeto de estrutura de
dados e projeto de algoritmos. No entanto, apesar das citadas diferenças serem mais evidentes nos
estágios de projeto de estrutura de dados e projeto de algoritmos, o projeto de arquitetura de sistema
é vital para o projeto de qualquer software, principalmente porque requisitos não funcionais como
desempenho (relevantes em sistemas paralelos) podem ser afetados nesse estágio. Sobre a
arquitetura de sistema, considera-se o modelo estrutural, onde há uma preocupação em como um
sistema é decomposto em subsistemas, e o modelo de controle, complementar ao modelo estrutural
e que diz respeito ao controle de fluxo entre os sistemas. Apesar de não ter sido observada a
necessidade de aplicar de forma abrangente esses modelos no estudo de caso do presente TCC, é
válido comentar sua relevância para o desenvolvimento de programas paralelos, uma vez que uma
das abordagens para o controle de fluxo entre os subsistemas é baseada em eventos, onde podem ser
50
aplicados em sistemas de tempo real e, portanto, serem relevantes no projeto de aspectos paralelos.
É possível no projeto de arquitetura decompor os subsistemas (abordados no modelo estrutural) em
componentes menores denominados módulos. O modelo orientado a objetos pode ser utilizado na
decomposição de um subsistema em módulos. Sommerville (2003) cita que o projeto de software
pode ser abordado de forma metódica por meio dos métodos estruturados, que são conjuntos de
notações e diretrizes para o projeto de software. Entre esses métodos, encontra-se o método
orientado a objetos (adotado neste TCC), que inclui um modelo de herança do sistema, modelos dos
relacionamentos estáticos e dinâmicos entre objetos e um modelo de como os objetos interagem
entre si quando o sistema está em execução. Esse método pode ser aplicado ao desenvolvimento de
programas paralelos.
Para a modelagem no paradigma paralelo, é necessário que alguns fatores sejam
considerados. A implementação do estudo de caso do presente trabalho cria n threads de forma a
processar o cálculo das rotas do grafo em paralelo. O valor de n é proporcional ao número de
processadores (núcleos) disponíveis no sistema. O trecho de código apresentado na Figura 12 ilustra
essa operação:
Figura 11: código Java para processos paralelos
São criadas então n threads para n núcleos (disponibilizado pelo método
“availableProcessors( )”). Cada thread é representada por uma classe denominada Task (vide
Figura 13).
Figura 12. classe Task
51
A seguir, é apresentada uma discussão sobre a forma que as threads devem “caminhar”
sobre o grafo a fim de processar em paralelo as rotas.
Paralelização das rotas
Para “caminhar” sobre as arestas do grafo é necessário saber se determinada coordenada foi
visitada ou não. Assim, compartilhar a mesma estrutura (ou seja, o mesmo grafo) entre várias
threads pode ocasionar um comportamento indesejado. Mesmo se utilizar algum mecanismo de
sincronização como monitores, por exemplo, ainda assim pode ocorrer de uma coordenada já ter
sido visitada por uma thread_1 e não ser visitada, portanto, pela thread_2, justamente porque essa
consultará o grafo a fim de saber se foi visitada e o grafo responder positivamente. Assim, a
thread_2 não visitará aquela coordenada. Nesse caso, deveria haver algum mecanismo que
verificasse se determinada coordenada já foi visitada pela thread ativa. No entanto, isso pode
envolver uma complexidade algorítmica maior e comprometer a eficiência do algoritmo adotado
(DFS).
Outra possibilidade é duplicar o grafo e distribuir essas cópias entre duas threads (o número
de cópias também poderia ser maior se for considerado um número maior de threads). No entanto,
isso não traria ganhos substanciais podendo ocorrer justamente o efeito contrário (e indesejado):
duas threads realizando o mesmo trabalho por unidade de tempo com a possibilidade de competir
por recursos. A solução a ser adotada e que pode trazer ganhos de desempenho é duplicar o grafo e
dividir pelo número de threads as rotas a serem processadas. Exemplo: considerando um grafo
hipotético com 5 coordenadas, percebe-se que o número total de rotas a ser processadas é (n-1)!,
resultando em um total de 24 rotas. Uma thread_1 será a responsável por processar as 12 primeiras
rotas deixando as demais a cargo da thread_2. Deveria, no entanto, que garantir que uma thread não
processasse as mesmas rotas da thread concorrente, pois do contrário, causar-se-ia o mesmo efeito
apresentado no parágrafo anterior. Assim, há a necessidade de implementar algum mecanismo que
indique às threads as arestas iniciais a serem percorridas. Considerando o exemplo anterior, as
arestas iniciais que podem ser formadas a partir da origem são: (A,B), (A,C), (A,D), (A,E). A
thread_1 processaria as rotas a partir das arestas (A,B) e (A,C) enquanto que a thread_2 processaria
as rotas a partir das arestas (A,D) e (A,E). Assim, o número de rotas que cada thread executaria
seria igual a (n – 1) !/ 2 resultando em um total de 12 rotas para cada thread. Se considerar que
cada núcleo do processador multicore executará, exclusivamente e sem interrupções, uma, e
somente uma, thread, poder-se-ia afirmar que haveria um ganho de 50% no tempo de execução. Por
exemplo: se para o cálculo de 24 rotas determinada thread necessita de 100 unidades de tempo,
52
pode-se concluir que para a metade do trabalho, ou seja, para 12 rotas o tempo necessário seria
justamente a metade, necessitando apenas de 50 unidades de tempo. Como haveria duas threads
executando em paralelo, em 50 unidades de tempo é possível processar as 24 rotas.
Esse cálculo é hipotético e pouco real, uma vez que dificilmente haverá exclusividade de
núcleo para cada thread (mesmo configurando cada thread com prioridade máxima). Do contrário,
seria necessário configurar cada thread com prioridade máxima evitando então que o sistema
responda a qualquer evento do usuário (por exemplo, o usuário não conseguiria parar a execução do
programa, pois os núcleos do processador multicore estariam exclusivamente ocupados com o
processamento além de ocorrer o efeito do “congelamento” da interface gráfica). Esse cenário não é
desejável para este estudo de caso, cujo objetivo é tornar a interface gráfica disponível para
qualquer ação do usuário. Mesmo que fosse alocado àquela thread de prioridade máxima
determinado processador, ainda assim não há garantias. Isso ocorre porque o sistema operacional
obedece ao seu algoritmo de escalonamento. As prioridades apenas “influenciam”, mas não há
garantias. Por último, deve ser considerada a impossibilidade de se chegar ao ganho perfeito, ou
seja 1:1, em razão do componente seqüencial contemplada pela lei de Amdahl.
Identificação de processos paralelos
Barney (2007) cita que o projeto de software paralelo consiste, primeiramente, em
compreender o problema e identificar as frações paralelizáveis do software. A identificação consiste
em observar o grau de dependência entre as variáveis envolvidas em um determinado trecho do
software. Como exemplo, considera-se o cálculo da série de Fibonacci através da fórmula F(k + 2)
= F(k + 1) + F(k). Esses três termos não podem ser calculados independentemente e, portanto, não
são passíveis de paralelização.
No caso específico do estudo de caso, o cálculo das rotas pode ocorrer de forma paralela
desde que observados alguns parâmetros. Entre tais, está a definição da origem do sistema com o
respectivo cálculo em paralelo das rotas a partir de cada nodo filho daquela origem (considerando
aqui a estruturação do sistema em um grafo). Por exemplo: considerando o problema do Caixeiro-
Viajante, supõe-se que há quatro cidades a serem visitadas (por exemplo, cidades “A”, “B”, “C” e
“D”). Essas rotas podem ser representadas em um grafo, conforme mostrado na Figura 11 (por
motivos de simplicidade, na figura não está mostrada a aresta que liga o último nodo (ou cidade)
visitado à origem. No entanto, para fins de cálculo do problema do caixeiro viajante, tal aresta deve
ser considerada). A oportunidade de paralelização consiste em calcular as rotas a partir da origem
como se fossem threads independentes, representados na Figura 11 como T1, T2 e T3. É possível
53
ainda paralelizar essas mesmas threads em linhas de execução menores como, por exemplo, a
paralelização entre o caminho “B, C, D” e “B, D, C”. Porém, dado os objetivos do presente TCC,
esse cenário não será considerado.
Em um programa, pode haver várias oportunidades de paralelização. Barney (2007) cita que
os esforços em paralelizar determinada atividade deve ser focada naquelas onde há uma carga de
trabalho considerável, cujos esforços em paralelizar se justifiquem com ganhos significativos de
desempenho. Cita ainda que ferramentas de análise de desempenho são úteis nesse momento. As
atividades passíveis de paralelização, mas que fazem pouco uso do processador podem ser
ignoradas. Este estudo de caso possui como atividade de maior relevância (no que diz respeito ao
uso do processador) o processamento das rotas. A Figura 11 apresenta um modelo de grafo utilizado
para o referido processamento.
Sendo assim, tal modelo é passível de paralelização, pois possui oportunidades
paralelizáveis e justifica-se pelo uso excessivo do processador. Outro item refere-se aos gargalos do
sistema. Pode ocorrer do programa conter algumas frações que provoquem alguma degradação no
desempenho tornando-se um gargalo no sistema. À primeira vista, pode-se considerar como sendo
essa fração uma oportunidade paralelizável. No entanto, deve-se recorrer à análise algorítmica em
um primeiro momento visando melhorar o desempenho por meio da utilização de algoritmos mais
eficientes.
Figura 13. Grafo para o problema do caixeiro-viajante
Portanto, pode-se fazer uma síntese dessa análise de programas paralelos como:
54
1) Identificar oportunidades paralelizáveis e os possíveis inibidores de paralelismo devido à
dependência de dados (exemplo, série de Fibonacci);
2) Análise de carga de trabalho; e
3) Análise algorítmica (investigação de algoritmos mais eficientes)
Pode-se mapear esses conceitos para o estudo de caso, da seguinte forma:
1) Pela análise de um grafo similar ao apresentado na Figura 11, percebe-se que o
processamento de determinada rota (A, B, C, D) independe do processamento de outra
rota qualquer (A, D, C, B). Pode-se considerar esses processamentos como
independentes entre si, sendo considerado como atividades passíveis de paralelização;
2) Ao executar o algoritmo de processamento de rotas, observa-se que há uma carga
considerável de processamento. Isso foi constatado ao observar o comportamento de
processador específico em uma máquina composta por uma arquitetura específica4
através do utilitário “Task Manager” do sistema operacional Windows XP, da Microsoft
3) O algoritmo utilizado para o processamento de rotas foi baseado no algoritmo DFS
(Depth-Fisrt Search ou Busca por Profundidade)
A análise culminou na criação de uma classe denominada “ProcessaRotas”. Essa classe é
responsável por processar todas as rotas entre as coordenadas (cuja quantidade definiu-se como
sendo entre 2 e 10, ficando a critério do usuário a escolha) do grafo. A UML dispõe de mecanismos
para modelar a classe “ProcessaRotas” que, até o momento, não difere dos modelos atuais de
modelagem de software. Exeção é a palavra “concurrent” que indica que o método por essa palavra
referenciada deve ser executada em paralelo.
A classe “ProcessaRotas” possui como atributo um grafo e um método denominado
“execute( )” (responsável por implementar e executar o algoritmo DFS ) que é executado
paralelamente por n threads disponíveis no sistema (vide Figura 12). O atributo “grafo”,
identificado no processo de análise, é composto por arestas e ambos podem ser modelados como
classes de entidade. No entanto, como essas classes não têm impacto direto no processamento
4 A arquitetura referenciada é composta por um processador Intel Core 2 Duo E6700, 2 GB de RAM e FSB de 800 MHz
55
paralelo, a ênfase será dada à classe “ProcessaRotas”. A diferença em relação ao aspecto seqüencial
fica por conta dos objetos ativos.
Figura 14. Método executado em paralelo
Descrição do problema
O problema supõe que um caixeiro viajante deseja visitar N cidades (vértices) de certa
localização e que, entre alguns pares de cidades existem rotas (arcos ou arestas), através das quais
ele pode viajar a partir de uma cidade para outra passando por cada uma das N cidades apenas uma
vez . O cálculo das rotas devem ser paralelizadas.
Restrições:
1. O problema deve ser representado por meio de um grafo.
2. As questões relativas ao cálculo da aresta (rota) de menor custo não devem ser consideras.
3. Deve-se explorar paralelamente (baseado nos critérios de caminhamento do algoritmo do
caixeiro viajante) todas as permutações (rotas) possíveis dos vértices do grafo.
Requisitos Funcionais 1. O sistema deve traçar todas as combinações de rotas existentes entre as coordenadas
selecionadas pelo usuário
2. O sistema deve processar as rotas paralelamente
3. O sistema deve apresentar graficamente cada rota processada
4. O sistema deve indicar o andamento do processamento
56
Requisitos Não-Funcionais 1. O sistema deve permitir que o processamento seja encerrado a qualquer momento pelo usuário
2. O processamento das rotas deve ocorrer em paralelo à sua apresentação gráfica
3. O processamento das rotas deve ocorrer em paralelo ao indicador de processamento
4. O processamento deve possuir melhor desempenho que o processamento seqüencial
Requisitos de Interface 1. Deve existir um botão que possibilite o término do processamento pelo usuário
2. Deve existir uma barra de progresso que indique o status do processamento
Caso de uso : traça rotas
Atores : usuário
Descrição : usuário solicita ao sistema que gere todas as rotas existentes entre todas as
coordenadas por ele (usuário) selecionadas apresentando graficamente cada rota e um indicador de
processamento.
Fluxo principal
1. O usuário seleciona o número de coordenadas
2. O sistema inicia o processamento de todas as rotas possíveis entre as coordenadas.
2.1. O sistema apresenta (traça) um gráfico para cada rota processada
2.2. O sistema atualiza o indicador de processamento a partir de cada rota processada
3. O sistema encerra o processamento
Fluxo de exceção
1. Se no passo 2 do fluxo principal o usuário decidir por encerrar o processamento do sistema,
esse (sistema) deve encerrar a apresentação do gráfico imediatamente e liberar os recursos
utilizados.
58
Diagrama de seqüência
4. AVALIAÇÃO E TRABALHOS FUTUROS
A principal métrica a ser adotada neste trabalho está relacionada à complexidade. Ao medir
a complexidade do software é possível prever e compreender os fatores que provocam a baixa
produtividade (LAIRD & BRENNAN, 2006). A complexidade estrutural aborda o projeto e a
estrutura do software em si. Há várias métricas de complexidade estrutural. Neste trabalho será
adotada a métrica estrutural orientada a objetos, que mensura as diferentes estruturas de programas
orientado a objetos. Pretende-se com essas métricas mensurar classes, modularidade,
encapsulamento, herança e abstração.
As métricas em questão formam um conjunto conhecido como métricas Chidamber e
Kemerer (CK) (LAIRD & BRENNAN, 2006). Esse conjunto consiste de seis métricas que são
utilizadas para mensurar o tamanho e a complexidade de classes, o uso da herança, o acoplamento
entre as classe, a coesão de classes e a colaboração entre as classes. As métricas adotadas são:
1. WMC (Weighted Methods per Class): foca o número e a complexidade de métodos
dentro da classe. Valores altos sugere que a classe deve ser dividida (ou refatorada) em
classes menores;
59
2. DIT (Depth of Inheritance Tree): mensura o grau hirárquico de uma dada classe. Valores
altos implicam em maior complexidade de projeto;
3. NOC (Number of Children): mensura o número de sucessores imediatos (classes filha)
de uma dada classe. Valores altos indicam que a abstração das classes pai estão diluídas
e, portanto, deve ser considerado rever o projeto;
4. CBO (Coupling Between Object Classes): mensura quantas classes se relacionam com
uma dada classe. Valores altos implicam em maior complexidade, manutenibilidade e
reusabilidade reduzidas;
5. RFC (Response for Class): mensura o número total de métodos potenciais a serem
executados em uma classe quando essa recebe uma mensagem e deve, por sua vez,
responder. Valores altos indicam complexidade maior; e
6. LCOM (Lack of Cohesion on Methods): mensura o número total de métodos dentro de
uma classe que referenciam uma dada variável de instância. A complexidade e
dificuldade de projeto crescem quando essa métrica é incrementada.
A modelagem do estudo de caso baseado no paradigma seqüencial culminou na criação das
classes descritas na tabela 3. As classes identificadas na modelagem baseada no paradigma paralelo
constam na tabela 4. As métricas apresentadas anteriormente foram aplicadas sobre as classes
identificadas a fim de mensurar sua complexidade baseado nos critérios que compõe aquelas
métricas:
Tabela 4: Aplicação de métricas (paradigma seqüencial)
Classe WMC DIT NOC CBO RFC LCOM
Controladora 1 0 0 2 1 1
ProjetaGrafo 4 0 0 2 1 1
ProcessaRotas 4 0 0 2 1 2
Graph 4 0 0 4 0 3
Edge 2 0 0 2 0 0
Vertex 6 0 0 2 0 0
Total 21 0 0 14 3 7
60
Tabela 5: Aplicação de métricas (paradigma paralelo)
Classe WMC DIT NOC CBO RFC LCOM
Controladora 1 0 0 2 1 1
ProjetaGrafo 3 0 0 2 1 1
ProcessaRotas 5 0 0 3 1 2
Graph 6 0 0 5 0 6
Edge 2 0 0 2 0 0
Vertex 4 0 0 2 0 0
SharedVars 5 0 0 1 1 0
EdgeLocker 3 0 0 1 1 0
Task 4 1 0 4 2 4
Total 33 1 0 22 7 14
É importante observar que a aplicação das métricas e os resultados conseguidos são válidos
exclusivamente para o estudo de caso deste trabalho. Portanto, os resultados devem ser avaliados
sob a ótica discursiva e não conclusiva.
De posse desses índices, percebe-se que no paradigma paralelo:
• Houve aproximadamente um aumento de 33% do número de classes no sistema em relação
ao paradigma seqüencial. Esse aumento é provocado pela inserção de classes responsáveis
pelo paralelismo por meio de threads (classe Task) e de classes que implementam a lógica
de monitores (classe SharedVars) e locks (classe EdgeLocker). Pode-se observar que essas
classes implementam a estrutura básica para o paralelismo. Trabalhos futuros poderiam
realizar testes em larga escala a fim de verificar que esse aumento tende a estabilizar quando
o sistema atinge tamanho de aproximadamente 35% maior que o sistema baseado no
paradigma seqüencial. Pra tal, seria necessário adotar métricas relacionadas ao tamanho do
software.
61
• A métrica WMC avalia a complexidade em relação ao número de métodos em uma classe,
indicando que valores altos sugerem que a classe deve ser dividida (ou refatorada) em
classes menores. Verifica-se que em algumas classes a complexidade diminui enquanto que
em outras, aumentam. Considerando a média entre as classes, conclui-se que no paradigma
seqüencial há uma média de complexidade igual a 3,5 por classe. No paradigma paralelo há
uma média de complexidade de 3,6 por classe. Assim, para este estudo de caso, o acréscimo
de classes necessárias à paralelização não trouxe aumentos significativos dos índices de
complexidade de acordo com a métrica WMC
• A métrica DIT refere-se ao grau hierárquica entre as classes e a complexidade agregada a
essa hierarquia. O estudo de caso não propiciou alterações relevantes nesse item de forma
que não é possível discutir essa métrica pela ausência de dados significativos. A métrica
NOC é proporcional à DIT e, por esse motivo, também não propicia discussões.
• A métrica CBO está relacionada ao acoplamento entre as classes. Valores altos indicam alto
acoplamento. Ao observar individualmente as classes que participam de ambos os
paradigmas, percebe-se que não houve alterações significativas dos índices. O mesmo é
válido ao avaliar as médias. Em ambos os paradigmas, a média foi igual a 2,4. Ou seja, a
adição de classes necessárias ao paralelismo não aumentou significativamente o
acoplamento em geral das classes.
• A métrica RFC indica o número total de métodos potenciais a serem executados em uma
classe quando essa recebe uma mensagem e deve, por sua vez, responder. Valores altos
indicam complexidade maior. Considerando as médias, verificou que não houve aumento
significativo de complexidade do paradigma paralelo em relação ao seqüencial
• A métrica LCOM refere-se à coesão das classes e indica, em valores altos, o aumento da
dificuldade de projeto e da complexidade. Essa métrica foi a que apresentou a maior
variação entre os paradigmas não sendo, no entanto, suficiente para concluir que o
paradigma paralelo acarreta em menor coesão das classes em geral. No paradigma
seqüencial, a média foi de 1,1 e no paralelo foi de 1,5.
As métricas CK auxiliam na avaliação de alguns aspectos relacionados ao desenvolvimento de
software, como a produtividade, o esforço dispensando no processo de reutilização e projeto de
62
classes, na dificuldade em implementar classes e manutenibilidade. Ao analisar os índices
conseguidos por meio das métricas apresentadas, verifica-se exclusivamente para o estudo de
caso deste trabalho que:
• A produtividade está relacionada não somente à complexidade do software; o esforço
também deve ser considerado. Portanto, não é possível apenas com a complexidade
concluir que a produtividade é afetada quando desenvolve-se software paralelo, mesmo
não havendo diferenças significativas entre os paradigmas seqüencial e paralelo.
• As reutilização não pôde ser avaliada pois está diretamente associada à métrica DIT e
NOC, não consideradas nessa avaliação devido à pouca disponibilidade de dados
• A dificuldade em implementar e projetar classes está associada diretamente com a
métrica LCOM, que mensura principalmente a coesão das classes. Houve uma diferença
entre os paradigmas, o que justifica a maior dificuldade em projetar software paralelo
(uma vez que esse obteve índices de complexidade mais altos). No entanto, é necessário
que trabalhos futuros apliquem essa métrica em larga escala a fim de verificar uma
tendência na dificuldade em projetar software paralelo
• A manutenibilidade está relacionada principalmente com a métrica CBO, que indica o
grau de acoplamento entre as classes. Não foram identificadas diferenças significativas
entre os paradigmas seqüencial e paralelo, de forma que pode-se sugerir uma discussão a
respeito dos reais impactos na manutenibilidade do sistema que programas paralelos
possam ocasionar. No caso específico deste trabalho, verifica-se que a manutenibilidade
não é atingida quando paraleliza-se o programa.
Apesar do estudo de caso não apresentar complexidade suficiente ao ponto de criar vários
cenários de use cases, foi possível aplicar as principais técnicas de análise para o estudo de caso em
questão, acarretando na produção do caso de uso “traça rotas”. As mesmas técnicas adotadas em
sistemas seqüenciais foram aplicadas para a produção de software paralelo. As características
específicas do paralelismo devem ser abordadas principalmente no levantamento de requisitos não
funcionais, onde questões como desempenho e consistência de dados são críticos. A descrição de
cenários geralmente são em nível alto de abstração. Os aspectos de mais baixo nível (paralelismo de
dados, fluxos de execução, etc.) são abstraídos. Esses aspectos ficaram mais evidentes na fase de
projeto e na implementação. Na fase de projeto, percebeu-se que as diferenças entre os aspectos
63
seqüenciais e paralelos ficam mais evidentes quando o nível de abstração é mais baixo. Essa
sentença contextualiza-se na seguinte citação de Sommerville (2003):
“os projetistas devem evitar, se possível, tomar decisões
prematuras sobre a simultaneidade (...). É melhor
decompor o sistema em módulos e, então, decidir durante a
implementação se eles devem ser executados em seqüência
ou em paralelo”.
TRABALHOS FUTUROS
A adoção de métricas é crucial para avaliar as técnicas de engenharia de software. Essas
métricas abordam o tamanho, a complexidade e o esforço para produzir software. Verificou-se que
a avaliação deve ser abrangente a fim de generalizar alguns aspectos como a produtividade, por
exemplo. Essa abrangência pode sugerir uma tendência que indique o grau de dificuldade em
produzir software paralelo em relação ao seqüencial. A aplicação das métricas CK e a conseqüente
avaliação apresentada neste trabalho é específica para o estudo de caso do caixeiro-viajante.
Portanto, trabalhos futuros poderiam aplicar métricas de tamanho, complexidade e esforço em uma
gama maior de problemas passíveis de paralelização a fim de obter índices gerais de produtividade
de software paralelo.
64
REFERÊNCIAS BIBLIOGRÁFICAS
ADL-TABATAI, Ali-Reza; KOZYRAKIS, Christos; SAHA, Bratin. A transactional programming in a multi-core environment. 2006. Disponível em: < http://csl.stanford.edu/~christos/publications/2006.unlocking_concurrency.queue.pdf >. Acesso em: 29 out. 2007. ADL-TABATABAI, Ali-Reza; KOZYRAKIS, Christos; SAHA, Bratin. Unlocking Concurrency. 2006. Disponível em: < http://www.acmqueue.org/modules.php?name=Content&pa=showpage&pid=444&page=4 > Acesso em: 15 nov. 2007. AMDAHL, Gene. Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities. AFIPS Conference Proceedings, 1967. BARNEY, Blaise. Introduction to Parallel Computing. 2007. Disponível em: <https://computing.llnl.gov/tutorials/parallel_comp/>. Acesso em: 16 jun. 2008. BOOCH, Grady; RUMBAUGH, James; JACOBSON, Ivar. UML: guia do usuário. 2. ed. Rio de Janeiro: Campus, 2006. CARDOSO, Bruno; ROSA, Sávio R.A.; FERNANDES, Tiago M. Multicore. Campinas, 2006. Disponível em: < http://www.ic.unicamp.br/~rodolfo/Cursos/mc722/2s2005/Trabalho/g07-multicore.pdf>. Acesso em: 15 set. 2007. CHARÃO, Andréa S. Análise de desempenho de programas paralelos. Santa Maria, 2006. Disponível em: < http://www-usr.inf.ufsm.br/~andrea/elc139/slides-analise-desempenho-2006b.pdf>. Acesso em: 12 out. 2007. COLOURIS, George. Sistemas distribuídos: conceitos e projetos. 4. ed. Porto Alegre: Bookman, 2007. COM CIÊNCIA. O futuro à computação pertence. Disponível em: <http://www.comciencia.br/comciencia/handler.php?section=3¬icia=268> Acessado em: 08 set. 2007. ERIKSSON, H.; PENKER, M.; LYONS B.; FADO, D.; UML 2 Toolkit. Indianapolis, US: Wiley Publishing Inc., 2004. FAZZIO, A. Alguns aspectos da nanoeletrônica molecular. São Paulo, 2004. Disponível em: <http://www.cepa.if.usp.br/e-fisica/divulgacao/oqueefisica/fazzio.php>. Acesso em: 30 out. 2007 FOWLER, Martin. UML Essencial. 3. ed. Porto Alegre: Bookman, 2005.
65
GRUNE, Dick et al. Projeto moderno de compiladores: implementação e aplicações. Rio de Janeiro: Campus, 2001. HERCKERT, Matheus G.H. Fluidodinâmica computacional e suas aplicações. Uberlândia, 2004. Disponível em: < http://br.monografias.com/trabalhos/fluidodinamica/fluidodinamica.shtml > Acesso em: 16 nov. 2007. HENNESSY, John L. ; PATTERSON, David A. Arquitetura de computadores: uma abordagem quantitativa. 3. ed. Rio de Janeiro: Campus, 2003. INTEL. Intel Multi-core Briefing. [S.I.], 2005. Disponível em: < http://download.intel.com/pressroom/kits/pentiumee/20050418presentation.pdf >. Acesso em: 12 out. 200 INTEL. Mudança radical: processamento multi-core abre possibilidades de negócio inovadoras. Disponível em: < http://developer.intel.com/portugues/technology/magazine/computing/multi-core-software-1006.htm>. Acesso em: 21 out. 2007. INTEL. Processadores Core 2 Duo. Disponível em: < http://www.intel.com/portugues/products/processor/core2duo/index.htm >. Acesso em: 25 set. 2007. LAIRD, Linda M., BRENNAN, M. Carol. Software measurement and estimation: a practical approach. New Jersy: Wiley-Interscience, 2006. LOUDON, Kyle. Dominando algoritmos com C. Rio de Janeiro: Ciência Moderna Ltda., 2000. MA, Josué T.H. Multicore. Campinas, 2006. Disponível em: < http://www.ic.unicamp.br/~rodolfo/Cursos/mo401/2s2005/Trabalho/049180-multicores.pdf>. Acesso em: 23 set. 2007. MACHADO, F.B.; MAIA, L.P. Arquitetura de Sistemas Operacionais. 3.ed. Rio de Janeiro: Livros Técnicos e Científicos, 2002. PATTERSON, David A.; HENNESSY, John L. Organização e projeto de computadores: a interface hardware/software. 2. ed., Rio de Janeiro: Livros Técnicos e Científicos, 2000. SILBERSCHATZ, Abraham. Fundamentos de Sistemas Operacionais. 6. ed. Rio de Janeiro: Livros Técnicos e Científicos, 2004 SILVEIRA, J.F Porto. Problema do Caixeiro Viajante .2000. Disponível em: <http://www.mat.ufrgs.br/~portosil/caixeiro.html>. Acesso em: 09 maio 2008. SEBESTA, Robert W. Conceitos de linguagens de programação. 4. ed. Porto Alegre: Bookman, 2000. SHIEL, Humphrey. Is your code ready for the next wave in commodity computing?
66
JavaWorld, 2006. Disponível em: < http://www.javaworld.com/javaworld/jw-07-2006/jw-0710-multicore.html > Acesso em: 27 out. 2007 SOMMERVILLE, Ian. Engenharia de Software. 6. ed. São Paulo: Pearson Education do Brasil, 2003. TANENBAUM, Andrew S. Sistemas Operacionais Modernos. 2. ed. São Paulo: Pearson Education do Brasil, 2003. TANENBAUM, Andrew S. Organização Estruturada de Computadores. 4. ed. Rio de Janeiro: Livros Técnicos e Científicos, 2001.
WIKIPEDIA. Parallel computing. Disponível em: < http://en.wikipedia.org/wiki/Parallel_Computing#Parallel_programming_models > Acessado em: 01 nov. 2007
WIKIPEDIA. Amdahl's law. Disponível em: < http://en.wikipedia.org/wiki/Amdahl%27s_law > Acessado em: 04 nov. 2007.