Comparação entre Java e C++ na Computação Numérica · Uma das vantagens está no fato de que...
Transcript of Comparação entre Java e C++ na Computação Numérica · Uma das vantagens está no fato de que...
Foz do Iguaçu, 27 a 29 de Outubro de 2004
Comparação entre Java e C++ na Computação Numérica
Claudio Schepke, Andrea Schwertner Charão Universidade Federal de Santa Maria
Laboratório de Sistemas de Computação Informática/CT - UFSM Campus - 97105-900, Santa Maria, RS
{schepke, andrea}@inf.ufsm.br
Resumo
A computação numérica é uma importante ferramenta para a solução de problemas nas áreas cientfjicas e da engenharia. Muitas aplicações nestas áreas requerem a resolução de grandes sistemas lineares de forma rápida e eficiente, geralmente através de métodos numéricos iterativos. A programação orientada a objetos torna a implementação desses métodos mais simples, uma vez que permite codificar um algoritmo em um n(vel de abstração bastante próximo à formulação matemática do método. O presente artigo apresenta uma comparação entre as linguagens Java e C++ para a resolução da equação de Laplace através do método iterativo do Gradiente Conjugado. Tal comparação contribui, principalmente, para ressaltar as vantagens e desvantagens destas linguagens no contexto da computação cientfjica de alto desempenho.
1. Introdução
A computação científica está geralmente associada à resolução de diversos problemas nas áreas das ciências naturais e exatas, bem como nas engenharias. Exemplos deste tipo de problemas incluem o cálculo do deslocamento estelar, a simulação de determinadas condições climáticas da terra, a descoberta de novos medicamentos, o teste da aerodinâmica de carros e aviões e a determinação de locais viáveis para uma possível extração de petróleo [1). Todas estas aplicações requerem processamento de alto desempenho, o que depende fortemente da combinação de hardware e software utilizados.
A programação orientada a objetos é uma opção para o desenvolvimento de aplicações que resolvem problemas modelados matematicamente. Em particular, este paradigma favorece a construção de programas em um nível de abstração próximo às formulações matemáticas. No entanto, as linguagens que seguem este paradigma possuem
diferenças entre si, tanto em relação a seus recursos para o desenvolvimento de programas, como também ao desempenho na execução do código resultante.
Este artigo tem por objetivo apresentar uma comparação entre as linguagens Java e C++ no contexto da computação c.ientífica de alto desempenho. Para isso, foi escolhido um problema recorrente em muitas aplicações científicas: a resolução da equação de Laplace através do método numérico do Gradiente Conjugado. As próximas seções tratam do uso da orientação a objetos aplicada a métodos numéricos, do uso de Java para o alto desempenho e, finalmente, apresentam a descrição da implementação e os resultados obtidos.
2. Uso da orientação a objetos para o cálculo numérico
Uma vasta gama de fenômenos físicos pode ser modelada através de equações diferenciais parciais, sendo resolvíveis numericamente com o auxílio de técnicas de discretização. A utilização destas técnicas resulta geralmente em um sistema linear de equações algébricas, cuja solução pode ser determinada através de métodos diretos ou iterativos [7). A programação eficiente destes métodos de resolução de sistemas não é uma tarefa trivial, mas atualmente pode-se contar com uma variedade de bibliotecas que facilitam sua utilização [12) [3]. No desenvolvimento destas bibliotecas, um dos paradigmas que se revelam adequados é a programação orientada a objetos [8).
A orientação a objetos traz consigo algumas características interessantes para o desenvolvimento de aplicações de cálculo numérico. Uma das vantagens está no fato de que os problemas podem ser considerados objetos. Desta forma, os dados representam os atributos e os métodos numéricos aplicados representam as funcionalidades. Já o uso de classes facilita a organização do código-fonte. Assim os métodos podem compartilhar características para a solução de um problema, sem a ne-
208
Anais do 5° Workshop de Computação de Alto Desempenho, WSCAD 2004
cessidade de replicação de trechos de código. Também o encapsulamento ajuda na minimização da interdependência entre as partes do código, ocultando detalhes de implementação.
3. Java e Alto Desempenho
O desempenho não foi originalmente considerado um fator relevante no desenvolvimento da linguagem Java. De fato, geralmente aponta-se como principais características desta linguagem a orientação a objetos, o suporte à distribuição, a segurança e a portabilidade. Mesmo assim, mudanças vêm ocorrendo para poder-se usufruir dos pontos fortes da linguagem e, ao mesmo tempo, obter uma melhora da eficiência do código gerado [13]. Em geral, a opção pelo uso de Java ocorre principalmente para programas em rede e em especial à Web.
O uso de Java no processamento científico possui alguns limitantes [5], sendo que um deles é a falta de arrays de várias dimensões com um formato regular. No caso da implementação de uma matriz numérica, é necessário utilizar uma estrutura de vetor de vetores. É importante notar que operações com matrizes de· grande porte aplicadas à álgebra linear são algumas das operações mais comuns e com maior custo computacional em aplicações científicas.
Outra característica que tem impacto no desempenho de Java é a verificação de exceções, que garante robustez aos programas escritos nesta linguagem. Devido ao tempo destinado à realização dessa tarefa, geralmente ocorre uma perda de desempenho na execução geral de um programa. Java faz a verificação de ponteiros que estejam apontando para regiões de memória não alocadas ou ultrapassando os limites pré-definidos de um vetor. Também o coletor de lixo consome um tempo significativo do período de execução, mas oferece uma vantagem ao programador, pelo fato dele não precisar liberar explicitamente a memória.
Uma terceira característica a ser levada em conta no contexto da computação científica está na não existência de estruturas simples que possibilitam o trabalho com números complexos e operadores matemáticos específicos. A solução adotada é a utilização de classes especiais para a representação de objetos cujos tipos não estejam definidos como padrão da linguagem e a implementação de métodos específicos, como nas operações utilizadas sobre matrizes e vetores. Para atender as necessidades de determinados grupos de programadores, já foram criados pacotes especializados, como os que possibilitam o trabalho com álgebra linear e funções matemáticas com um alto nível de abstração sobre os dados manipulados [4]. Recentemente, foram feitas algumas melhorias em Java para a geração de código eficiente. Inicialmente, os bytecodes gerados na compilação se tomaram mais específicos, para, em seguida, poderem ser compilados para a arquite-
209
tura nativa na primeira execução do código. Atualmente, os compiladores permitem gerar código de máquina, diretamente do código-fonte Java, para uma determinada arquitetura, sendo possível encontrar vários compiladores para este fim. Uma solução alternativa está na possibilidade de serem feitas chamadas de métodos nativos, implementados em outra linguagem, dentro do código Java (JNI- Java Native Interface).
Outra melhoria está na tentativa de diminuição do tempo de comunicação entre processos. Uma das possíveis formas está na utilização de um código RMI (Remate Method lnvocation) mais eficiente [8]. Soluções como o RMI assíncrono não bloqueiam os processos nas chamadas de funções remotas, algo que permite "mascarar"o tempo de comunicação no caso de sistemas distribuídos. Existem também implementações para uma Máquina Virtual Distribuída, que oculta do usuário a existência de vários nós-processadores. Seu uso permite um grande paralelismo, já que os diversos fluxos de código podem ser executados sobre diversos processadores.
Ainda na área de paralelismo, uma das idéias mais recentes é a computação em grids [9]. O objetivo, neste caso, é permitir o processamento remoto em máquinas ociosas disponíveis de diversos lugares, enviando e recebendo tarefas e respostas por rede, para que grandes quantias de dados sejam processadas. O uso de Java na implementação de programas de gerenciamento de grids é um campo de pesquisa e desenvolvimento bastante ativo atualmente.
4. Comparação entre Java e C++
Esta seção visa apresentar uma comparação entre os tempos de execução de duas versões (Java e C++) de uma mesma aplicação. O objetivo desta comparação é apresentar uma diferença prática existente mi simulação de um código e, em especial, do método do Gradiente Conjugado, não buscando fazer uma análise teórica do custo das operações, já que estes foram amplamente discutidos [2]. No passado foram feitas diversas comparações com base no tempo de execução de cada uma das operações [ 11 ]. É preciso, entretanto, contextualizar as mudanças feitas, tanto na implementação, com o uso de bibliotecas específicas, como na geração de código por parte dos compiladores, analisando as suas principais partes.
A comparação feita neste trabalho tem como base uma aplicação cuja finalidade é determinar a solução de um problema modelado pela Equação de Laplace, utilizando o método do Gradiente Conjugado (GC) [7]. Esse método explora as propriedades das matrizes simétricas, positivas, definidas e, em especial, as matrizes esparsas, sendo um dos métodos numéricos iterativos mais utilizados. Suas operações se concentram em tomo de operações entre vetores, matrizes e vetores, e escalares com vetores. A cada
iteração são feitas duas operações entre matrizes e vetores, três produtos internos e três atualizações de vetor. O método tem como característica a rápida convergência.
Dentre as bibliotecas existentes para operações com álgebra linear para Java, foi escolhida a biblioteca JMP [10], versão 0.7.1, totalmente implementada nesta linguagem. As principais características de JMP se referem ao seu uso de matrizes esparsas, o oferecimento de soluções paralelas para as operações com matrizes e vetores, especialmente o BLAS (Basic Linear Algebra Subprograms), a disponibilidade de diversos pré-condicionadores, a facilidade para leitura de vetores e matrizes, bem como a possibilidade de decomposição, além de outros recursos para a criação de programas. Já para a implementação em C++, foi escolhida a biblioteca SparseLib++ [6], versão 1.5. O foco principal de SparseLib++ são as estruturas de dados e o suporte computacional a métodos iterativos. A fim de propiciar uma execução eficiente destes métodos, esta biblioteca possui uma vasta gama de formatos de armazenamento, como estruturas para matrizes e vetores compactados, o que pode diminuir o número de operações a serem aplicadas, bem como possibilitam ao programador optar por um formato que melhor se adapta ao problema. Também existem métodos de leitura para diversos formatos de compressão.
Para fins de comparação, foram gerados três códigos executáveis: C++, Java bytecode e Java compilado para código nativo. O código escrito nas duas linguagens é bastante semelhante, mudando apenas a forma como as bibliotecas resolvem o sistema. Para o código C++ foi usado o compilador GNU G++IGCC, versão 3.3.2. Para o código em Java foi utilizado o compilador Javac, com o JDK 1.4.02, e o compilador GNU GCJ/GCC, versão 3.3.2, para a geração de código nativo.
Os testes foram realizados em um computador com processador Intel Celeron de 1 GHz e memória de 128MB. As matrizes utilizadas foram de 2.000 por 2.000, 20.000 por 20.000 e 200.000 por 200.000 elementos. Após as execuções, obteve-se as médias das 1 O execuções realizadas para cada caso, sendo que o desvio padrão se apresentou baixo.
Em termos de medição dos tempos de execução, o programa foi dividido em três partes. Na primeira parte, é medido o tempo de inicialização das variáveis e o preenchimento dos vetores e matrizes. Em seguida, é calculado o tempo para a fase do pré-condicionamento. O précondicionamento consiste em simplificar a matriz, para que as operações feitas pelos métodos numéricos possam convergir de modo mais rápido. Sem o pré-condicionamento, os métodos seriam mais custosos para a aquisição da solução. Para esta aplicação, o pré-condicionador escolhido foi a fatorização LU incompleta [7]. Por fim, o módulo em que se tem o maior interesse é a parte
Foz do Iguaçu, 27 a 29 de Outubro de 2004
em que há a aplicação do método do Gradiente Conjugado. Além destas três regiões foi medido o tempo total de execução. Todos esses resultados estão apresentados nas tabelas e no gráfico a seguir.
210
Tabela 1 - Tempos de execução com C++
C++ 2000 20000 200000 Inicio 0.05012 0.63963 6.44687 Precond 0.00442 0.04990 0.49968 MétodoGC 0.00521 0.07280 0.72616 Total 0.06196 0.76465 8. 14305
Tabela 2- Tempos de execução com Java compilado
Java Compilado 2000 20000 200000 Inicio 0.0105 0.1402 1.0903 Precond 0.0552 12.1857 1823.1154 MétodoGC 0.0243 0.2198 1.9792 Total 0.0908 12.5467 1826.1864
Tabela 3- Tempos de execução com Java bytecode
Java bytecode 2000 20000 200000 Inicio 0.0953 0.7638 7.1611 Precond 0.2807 16.0573 1893.0888 MétodoGC 0.069 0.7331 6.8919 Total 0.4898 17.6046 1907.2011
8
7
6
~5 8.4 E Ql 3 ....
2
1
o 2000 20000 200000
Dimensão da Matriz
Figura 1. Comparação entre os tempos de execução do método GC
Anais do 5° Workshop de Computação de Alto Desempenho, WSCAD 2004
5. Discussão dos Resultados
O desempenho apresentado na comparação entre o método do Gradiente Conjugado, nas três execuções, apresenta bons resultados no uso de Java compilado para código nativo. À medida que aumenta o número de elementos da matriz, aumenta também a relação de eficiência de Java compilado sobre Java bytecode e C++ para o método em questão, ainda que a execução de C++ seja a melhor. Já os tempos de inicialização são semelhantes para Java bytecode e C++, melhorando para o código Java compilado, o que mostra um maior esforço por parte da implementação C++, para a criação das estruturas com as informações.
O pré-condicionamento, entretanto, é uma etapa que se mostra bastante penosa para a implementação em Java. O uso de pré-condicionadores é fundamental para uma rápida convergência dos métodos numéricos, já que os mesmos servem para se obter um bom valor inicial para a resposta. Bons pré-condicionadores conseguem diminuir bastante o tempo a ser gasto pelo método iterativo. Todavia, um grande número de operações precisa ocorrer para tanto. Devido às estruturas especiais de armazenamento disponíveis na biblioteca SparseLib++, no que diz respeito a matrizes esparsas, como também o fator natural do desempenho de C++ ser melhor de modo geral, o tempo do pré-condicionamento apresentou-se bem menor para este caso.
6. Conclusão
A utilização da orientação a objetos para operações relacionadas à computação numérica mostra-se como um recurso de programação viável através do uso de algumas bibliotecas. Atualmente existem muitas bibliotecas em C++ voltadas para o uso científico. Muitas delas são amplamente utilizadas e já foram alvo de otimizações. Este é o caso da SparseLib++, uma biblioteca que possui uma excelente implementação para as operações com matrizes esparsas.
O uso de Java como uma linguagem de programação para aplicações científicas tem-se mostrado como uma opção, apresentando a portabilidade e a abstração orientada a objetos como ponto forte. Mesmo assim, as bibliotecas em Java podem ainda ser consideradas escassas e as existentes são geralmente implementadas para aplicações específicas. No caso da biblioteca JMP, a mesma ainda está em desenvolvimento, sofrendo constantes revisões e incremento de funções. No entanto, seu desempenho ainda se apresenta inferior ao que é exigido pela computação científica. Cabe destacar que, no momento, existem muitas bibliotecas em desenvolvimento, tentando abranger mais funcionalidades [3].
A otimização de código na compilação, as melhorias na implementação nas funções de baixo nível , bem como as
n1
características.a serem melhoradas nas linguagens, poderão apresentar resultados significativos para os próximos anos. Todavia, uma solução mais usual pode ser o processamento paralelo para a obtenção de resultados em um tempo computacional menor.
Referências
[I) G. R. Andrews. Foundations of Multithreaded, Parai/e/, and Distributed Programming. Addison-Wesley, US, 200 I.
[2) M. Bergman and P. Sloot. Basic Linear Algebra Subsystems. 1993. Dept. ofComp. Sys., Univ. of Amsterdam, number CAMAS-TR-2.1.2.1, Department of Computer Systems, University of Amsterdam, The Netherlands.
[3) R. Boisvert and R. Pozo. JavaNumerics. 2004. http://math.nist.govljavanumerics.
[4] R. F. Boisvert, J. J. Dongarra, R. Pozo, K. A. Remington. and G. W. Stewart. Developing numericallibraries in Java. 199M. http://www.cs.ucsb.edulconferences/java981papersljnt.pdf.
[S] G. G. H. Cavalheiro. Princ(pios da Programaçcio Concorrente. ANAIS, Segunda Escola Regional de Alto Desempenho- Sociedade Brasileira de Computação - Instituto de Informática da UFRGS I UNISINOS I ULBRA. 2002.
[6) J. Dongarra, R. Pozo, K. Remington, X. Niu, and A. Lumsdaine. A sparse matrix library in C++ for high performance architectures. 1994. ftp://gams.nist.govlpublpozolpaperslsparse.ps.Z.
[7) J. J . Dongarra, L. S. Duff, D. C. Sorensen, and H. A. Van der Yorst. Numerical Linear Algebra for High-Performcmce Compllters. SIAM, 1998.
[8) J. Farley. Java Distributed Computing. O'Rei lly, 1998.
[9] I. Foster and C. Kesselman. The Grid: 8/ueprillfs for a New Computing lnfrastructure. Morgan Kaufmann, 1999.
[ lO] B. O. Heimsund. JMP - A sparse matrix library for Java, 2004. http://www.math.uib.nol bjomoh/jmplindex2.html.
[ li) A. G. Hoekstra, P. M. A. Sloot, M. J . De Haan, and L. Hertzberger. Time complexity analysis for distributed mcmory computers - Implementation of a parallel Conjugate Gradient method. 1991. In J. van. Leeuwen, editor, Computing Science in the Netherlands, pages 249-266. Elsevier Science.
[12) T. Yeldhuizen. The Object-Oriented Numerics Page, 2004. http://www.oonumerics.orgloon.
[ 13) P. Wu, S. Midkiff, J. Moreira, and M. Gupta. Effi-cient Support for Complex Numbers in Java. 1999. http://www.cs.ucsb.edulconferences/java991papersl53-wu.pdf.