Comparação entre Java e C++ na Computação - Inf

Propaganda
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 cientı́ficas
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 cientı́fica 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
cientı́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-
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 tornaram mais especı́ficos,
para, em seguida, poderem ser compilados para a arquite-
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 (Remote
Method Invocation) 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 na 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 torno de operações entre vetores, matrizes e vetores, e escalares com vetores. A cada
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.
Tabela 1 - Tempos de execução com C++
C++
Inicio
Precond
Método GC
Total
2000
0.05012
0.00442
0.00521
0.06196
20000
0.63963
0.04990
0.07280
0.76465
200000
6.44687
0.49968
0.72616
8.14305
Tabela 2 - Tempos de execução com Java compilado
Java Compilado
Inicio
Precond
Método GC
Total
2000
0.0105
0.0552
0.0243
0.0908
20000
0.1402
12.1857
0.2198
12.5467
200000
1.0903
1823.1154
1.9792
1826.1864
Tabela 3 - Tempos de execução com Java bytecode
Java bytecode
Inicio
Precond
Método GC
Total
8
Tempo (s)
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++/GCC, 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 128 MB.
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 10 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
2000
0.0953
0.2807
0.069
0.4898
20000
0.7638
16.0573
0.7331
17.6046
200000
7.1611
1893.0888
6.8919
1907.2011
C++
7
JavaCompilado
6
JavaBytecode
5
4
3
2
1
0
2000
20000
200000
Dimensão da Matriz
Figura 1. Comparação entre os tempos de
execução do método GC
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
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
[1] G. R. Andrews. Foundations of Multithreaded, Parallel, and
Distributed Programming. Addison-Wesley, US, 2001.
[2] M. Bergman and P. Sloot. Basic Linear Algebra Subsystems. 1993. Dept. of Comp. 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.gov/javanumerics.
[4] R. F. Boisvert, J. J. Dongarra, R. Pozo, K. A. Remington, and
G. W. Stewart. Developing numerical libraries in Java. 1998.
http://www.cs.ucsb.edu/conferences/java98/papers/jnt.pdf.
[5] G. G. H. Cavalheiro. Princı́pios da Programação Concorrente. ANAIS, Segunda Escola Regional de Alto Desempenho - Sociedade Brasileira de Computação - Instituto de Informática da UFRGS / UNISINOS / 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.gov/pub/pozo/papers/sparse.ps.Z.
[7] J. J. Dongarra, L. S. Duff, D. C. Sorensen, and H. A. Van
der Vorst. Numerical Linear Algebra for High-Performance
Computers. SIAM, 1998.
[8] J. Farley. Java Distributed Computing. O’Reilly, 1998.
[9] I. Foster and C. Kesselman. The Grid: Blueprints for a New
Computing Infrastructure. Morgan Kaufmann, 1999.
[10] B. O. Heimsund. JMP - A sparse matrix library for Java,
2004. http://www.math.uib.no/ bjornoh/jmp/index2.html.
[11] A. G. Hoekstra, P. M. A. Sloot, M. J. De Haan, and L. Hertzberger. Time complexity analysis for distributed memory
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. Veldhuizen. The Object-Oriented Numerics Page, 2004.
http://www.oonumerics.org/oon.
[13] P. Wu, S. Midkiff, J. Moreira, and M. Gupta.
Efficient Support for Complex Numbers in Java.
1999.
http://www.cs.ucsb.edu/conferences/java99/papers/53wu.pdf.
Download