¿”Java es lento”?
Java tuvo sus inicios en 1991, y
Sun Microsystems
lo liberó al público en 1995. Aunque el lenguaje heredó mucha de sus
sintaxis de C/C++, los objetivos de Java en aquél entonces pueden
resumirse en los siguientes principios:
• Simple, orientado a objetos y familiar.
• Robusto y seguro.
• Arquitectónicamente neutro y portable.
• Ejecutándose en “alto desempeño”.
• Interpretado, con soporte a paralelismo y dinámico.
El compilador de Java convierte el código fuente en archivos
bytecode, que posteriormente son interpretados por la
Máquina Virtual de Java o
JVM, en un
modelo de ejecución en pila o
stack.
Las primeras versiones de Java – previas a la 1.2 – no realizaban
optimizaciones en el bytecode y debido a que las diferentes versiones de
JVM eran más bien genéricas, la ejecución tenía un desempeño pobre.
A partir de la versión 1.2 (Diciembre de 1998), Java incluyó un compilador
Just-In-Time o
JIT,
que optimizaba el bytecode en tiempo real de acuerdo a la carga de
trabajo sobre el programa. Como contraste, un modelo de compilación
estática como C/C++ “adivina” dónde se encuentran los cuellos de botella
y se enfoca en esa parte del código para realizar la optimización, pero
en el caso de ambientes dinámicos como las aplicaciones web, el
compilador JIT mantiene una relativa ventaja. Por otro lado, con cada
nueva versión de Java, el lenguaje ha ido mejorando su desempeño, ya sea
por optimización en la JVM, el
recolector de basura o el compilador JIT. De acuerdo a
este artículo,
podemos ver la comparación en desempeño entre C++ y Java en algunas
funciones relativamente sencillas. Lo sorprendente es lo malo que se
desempeña C++ si durante la secuencia de compilación y
enlace (
linking)
se deshabilitan todas las banderas de optimización de código.
Adicionalmente, de la versión 6 a la 7, Java mejora su desempeño en
alrededor de un 33%:
| Función |
Ejemplo |
Java SE 6 (2010) |
Java SE 7 (2012) |
C++ (optimizado) |
C++ (sin optimizar) |
| Aritmética entera |
int Y = rand(X) * 2 |
25 |
16 |
0.001427 |
70 |
| Aritmética de punto flotante |
double Y = rand(X) * 2.0 |
47 |
28 |
0.001477 |
85 |
| Comparación de enteros |
if A == B then X = B else X = B |
50 |
33 |
0.001971 |
56 |
| Acceso a memoria indexada |
for i = 0 to N: int X = array[n] |
53 |
25 |
11 |
136 |
| Asignación de memoria – tipo primitivo |
for i = 0 to N: int array[n] = X |
12,994 |
7,589 |
3,661 |
8,567 |
| Asignación de memoria – objetos |
for i = 0 to N: Object array[n] = X |
710,788 |
191,581 |
29,348 |
70,436 |
Tiempos
de ejecución en milisegundos (excepto la asignación de memoria, dada en
nanosegundos) para algunas funciones en C++ y Java. La ejecución de J2SE
7 se realizó el 10/12/2012. Fuente: developer.com
Podemos deducir que el problema de desempeño no reside en el
lenguaje mismo, sino en el compilador y en su caso, la JVM. De hecho,
existen dos ejemplos de esta dependencia:
• Es bien sabido por muchos que
JRockit, la JVM construida por
BEA Systems (ahora Oracle) y publicada en 2002, es mucho más escalable en ambientes con una fuerte demanda de recursos que
HotSpot,
la JVM originalmente concebida por Sun Microsystems. Tanto así que la
próxima Java SE 8, por liberar en Septiembre de 2013, tendrá integrado
el
core de JRockit.
• Por el contrario, algunas versiones de C++ para Linux sobre
Intel pueden tener un nivel de optimización tan mediocre (por ejemplo,
G++ [
gcc] 3.3.1), que incluso con código escrito por el mejor programador, pueden tener un
desempeño
equivalente o más lento que el de Java, con lo que muchos fans de éste
lenguaje justifican la afirmación de que “Java es más rápido que C++”
(idea que por cierto, también es errónea).
Finalmente, debemos considerar la manera en que se está usando
el lenguaje cuando hablamos de desempeño. En el caso de Java, tenemos el
clásico ejemplo de
Vector vs. ArrayList: mientras un Vector es una
colección de datos
“segura” para implementar hilos de ejecución y paralelismo, ésta genera
un fuerte golpe al desempeño debido a que controla el acceso
concurrente sobre los elementos que ésta almacena. Por ello, tampoco es
sano realizar nuestras comparaciones sin primero verificar que el código
fuente está optimizado, ya que en algunos casos la programación es tan
mala que el tiempo de ejecución se incrementará enormemente.
Java vs. C++: una comparación histórica
¿Cómo se comparan Java y C++ en términos de programas “de la vida real”? Actualmente abundan muchos
microbenchmarks
como el arriba mostrado, donde se verifican funciones muy simples, como
manipulación de arreglos o acceso a memoria. Sin embargo, la mayoría de
los casos de prueba son demasiado básicos como para ser un indicador
confiable del desempeño. En la actualidad, existe un estudio comparativo
formal:
desarrollado por J. P. Lewis y Ulrich Neumann, investigadores de la
Universidad del Sur de California (USC),
el estudio se basa en 5 pruebas con diferentes versiones de Java y C++.
Sus resultados, en términos generales, fueron los siguientes:
• Al
comparar algoritmos numéricos como
FFT,
factorización de matrices o
SOR
en diferentes arquitecturas y compiladores, los investigadores
encontraron que el desempeño de Java en plataformas Intel es
razonablemente cercano al de C++ y que Java era más rápido que al menos
un compilador en C (
KAI sobre Linux Red Hat 5.0).
En hardware con Intel Pentium, especialmente con Linux, la diferencia
en desempeño es lo suficientemente pequeña como para carecer de
importancia.
• Al utilizar el benchmark
SciMark2 en una plataforma Intel, ellos encontraron que el IBM JDK 1.3.0 generaba un poder de procesamiento de 84.5
MFlops,
mientras que el compilador de linux2.2 gcc (2.9x) era marginalmente
mejor con 87.1 MFlops. Es decir, Java era tan sólo 3% más lento que C++.
• Utilizando implementaciones de métodos numéricos mediante
objetos, ellos encontraron que Java se aproximaba bastante a los tiempos
que tardaba C++ en ejecutarse. Por ejemplo, para generar el
Triángulo de Pascal, Java tardaba 9 milisegundos mientras C++ tardaba 1.1 segundos. Para una
Factorización LU, Java tardaba 1 segundo en resolver el problema mientras C++ tardaba hasta 3.9 segundos.
•
Implementando
microbenchmarks con y sin cache, los investigadores encontraron que
Java se encuentra justo a la mitad entre los mejores y los peores
compiladores de C++. Es decir, Java 3 es mejor que gcc 3.2, pero nunca
le ganará a un gcc 4.1.0.
El problema que tengo con este estudio es que fue desarrollado
hace ya algún tiempo – en aquél entonces, Lewis y Neumann compararon
Java 2 o 3 contra las versiones de C++ disponibles entre 2003 y 2004 –
por lo que si bien podemos darnos una mejor idea de que la velocidad de
los programas definitivamente depende del compilador, hoy por hoy
tendríamos que correr todas estas pruebas en plataformas
multi-core, con la última versión de software disponible para darnos cuenta cuál es el estatus actual de esta “carrera armamentista”.
Java 7 vs. C++ (gcc) 4.6.3
Afortunadamente, los amigos de
Debian han hecho su tarea de forma magnífica. Mediante
crowdsourcing, ellos han estado comparando no sólo Java y C++, sino también implementaciones de otros lenguajes como
Fortran,
Ruby o
Perl. Incluyendo
árboles binarios,
fractales de Mandelbrot o el
cálculo de Pi (π),
los resultados son bastante coherentes. Aunque las pruebas se
desarrollaron en diferentes plataformas, para este caso nos enfocaremos
en los resultados de correr estos programas en
Intel Q6600 (Quad Core) con
Ubuntu 5.10 x64:
| Prueba |
Java SE 7 |
C++ 4.6.3 |
Mejor de su tipo (factor = 1.0) |
| N-Body |
2.1 |
1.0 |
C++ |
| Fannkuch-redux |
1.9 |
1.4 |
ADA 2005 GNAT |
| Meteor-contest |
15.0 |
1.0 |
C++ |
| Fasta |
3.4 |
1.7 |
C |
| Spectral-norm |
2.3 |
1.0 |
Fortran Intel, ADA 2005 GNAT, C++ (empate) |
| Reverse-complement |
1.9 |
1.0 |
C++ |
| Mandelbrot |
1.9 |
1.7 |
Fortran Intel |
| K-nucleotide |
1.1 |
1.0 |
C++ |
| Regex-dna |
5.3 |
1.7 |
C |
| Pidigits |
2.4 |
1.2 |
ATS |
| Chameneos-redux |
7.8 |
1.1 |
ADA 2005 GNAT, C (empate) |
| Thread-ring |
41.0 |
21.0 |
Haskell GHC |
| Binary-trees |
1.6 |
1.6 |
C |
Factor de desempeño
en Java y C++ para diversos algoritmos matemáticos. El factor se mide
como 1.0 = tiempo con el mejor desempeño. En la tabla se muestra que
para el programa Fannkuch-redux, Java tardó 1.9 veces el mejor tiempo,
mientras que C++ tardó 1.4 veces el mejor tiempo. En este ejemplo, el
mejor tiempo se lo llevó el lenguaje ADA 2005 GNAT. Fuente: debian.org
Obteniendo la media, mejores y peores tiempos, también incluidos
en el estudio, nos damos cuenta que Java no está lejos de alcanzar la
velocidad de C++, aunque todavía permanece por debajo del desempeño
logrado por éste lenguaje y sus optimizaciones:
| Lenguaje |
Mejor Tiempo |
Peor Tiempo |
Media |
| C++ |
1.00 |
1.69 |
1.21 |
| Java 7 |
1.13 |
3.46 |
1.95 |
Comparación de desempeño para todas las pruebas en Java y C++. Fuente: debian.org
De hecho, si sólo tomamos los valores de la media, nos daremos
cuenta que el factor de desempeño de Java es de 3.46/1.69 = 2.06 contra
el de C++. Es decir, una relativamente amplia variedad de programas
escritos en Java tardarían aproximadamente el doble de lo que tardarían
si fueran escritos en C++. Esto es un precio a pagar muy pequeño si
tomamos en cuenta los otros beneficios de Java, como portabilidad,
facilidad de uso, o impedir que los programadores administren la memoria
directamente (
apuntadores, ¿alguien?).
Conclusiones
Aunque todavía le falta camino por recorrer, durante los
últimos años Java ha disminuido su factor de desempeño contra C++ hasta
alrededor del doble en plataformas Linux-Intel. Por lo que es necesario
“iluminar a los no iniciados” para que se den cuenta que la “lentitud de
Java” es una leyenda urbana que ha perdurado por más de 15 años sin un
verdadero análisis que lo respalde. Por otro lado, es importante
reconocer que ningún lenguaje es la “bala de plata”, por lo que Java o
C++ deben implementarse de acuerdo al problema que queremos resolver:
para acceder a recursos de bajo nivel como drivers, rutinas matemáticas o
código embebido de alto desempeño, la respuesta la encontramos en C++.
Para programas orientados a negocios o web que requieran facilidad de
modificación, portabilidad o simplemente, cuyos problemas de desempeño
puedan ser “matados a
billetazos” (es decir, pagando por agregar
hardware más potente), Java es una buena opción. Para finalizar, esto no
debería ser una competencia para ver “quién es el mejor”, sino
encontrar la manera de complementar ambos lenguajes. Después de todo,
ambos son los
más populares
entre los programadores. En nuestro caso particular, una opción
interesante será tener el procesamiento geoespacial en C++, mientras
dejamos todo lo demás en Java y sus múltiples
frameworks de desarrollo.