La idea es comparar la velocidad de un programa en C contra la de el mismo programa (algoritmo) en Java
#include <stdio.h>
int main(int argc, char* argv[]) {
long i;
long acc = 0;
for (i = 0; i < 1000000000; i++) {
if (i % 37 == 0 || i % 53 == 0) {
acc += i;
}
}
printf("%ld\n", acc);
return 0;
}
public class vel {
public static void main(String[] argv) {
long i;
long acc = 0;
for (i = 0; i < 1000000000; i++) {
if (i % 37 == 0 || i % 53 == 0) {
acc += i;
}
}
System.out.println("" + acc);
}
}
$ gcc -o vel vel.c
$ javac vel.java
$ time ./vel
22692504675420780
real0m48.872s
user0m48.059s
sys0m0.064s
$ time java -d64 vel
22692504675420780
real0m57.900s
user0m56.656s
sys0m0.068s
C le gana a Java, pero, por muy poco tiempo; no se trata de
"órdenes de magnitud" de diferencia. Ahora bien, el tiempo del
programa en C es discutible; no he usado optimización. Al
recompilar el programa con -O3
, el tiempo de ejecución baja a 7
segundos, así que C gana, ahora sí, por un orden de magnitud.
A favor de Java hay que decir que el programa de prueba es muy sencillo; es posible que los tiempos de ejecución sean más parejos cuando los programas sean más complejos (de hecho, hay buena evidencia de que es así).
A manera de prueba, quité las líneas #include <stdio.h>
y
printf("%ld\\n", acc);
del programa en C, y recompilé de nuevo con
la máxima optimización disponible; el tiempo de ejecución cayó a 2
milésimas de segundo. Esto puede verse de 2 formas:
C puede ser increíblemente rápido, si el programa es suficientemente sencillo.
Sólo las cosas sencillas tienen un tiempo de ejecución en C sensiblemente menor respecto a otros lenguajes.
Varios de ustedes se preguntarán porqué defiendo a Java, cuando he dicho varias veces que es una mugre de lenguaje. Y de hecho, lo es; pienso que Java (como lenguaje), es horrible. Pero últimamente he aprendido (muy, muy por encima) cómo se ejecuta Java, y le he tomado respeto a la máquina virtual de Java.
Nótese que estoy hablando de java, y no de javac; la diferencia en velocidad no la hace el compilador, sino la JVM; los programas en Java se compilan a bytecode de la JVM, pero la JVM toma dicho bytecode y lo recompila en tiempo de ejecución. Sí, un JIT. Ése es el truco.
Usar un JIT permite hacer una variedad de optimizaciones que sencillamente no son posibles en tiempo de compilación; por ejemplo, ver qué secciones del programa se ejecutan frecuentemente, para recompilarlas a assembler. Y muchas (si no todas) las optimizaciones que puede hacer un JIT están disponibles para cualquier tipo de lenguaje, bien sea estático (como Java, C#) o dinámico (Python, Lua).
De hecho, a la larga, los lenguajes dinámicos podrán ser más veloces que los estáticos. Para ser más precisos, los lenguajes que permiten mejores abstracciones serán más rápidos que aquellos que no. El motivo de esto es que, al usar operaciones de "más alto nivel", por llamarlas de alguna forma, se delega el problema de optimizar el código. Por ejemplo, considere los siguientes fragmentos de código:
my_list = [slow_function(i) for i in range(5000)]
for (i = 0; i < 5000; i++) {
my_list[i] = slow_function(i);
}
No hay nada que impida que un futuro intérprete de Python (capaz de
usar varios núcleos) vea ese código y decida usar varios hilos para
ejecutar varias invocaciones a slow_function
de manera simultánea.
La velocidad de ejecución habría aumentado varias veces, y lo mejor
es que no fue necesario modificar el código. Paralelismo
gratuito, cortesía de la máquina virtual que ejecuta el programa.
Del otro lado, no hay mucho que hacer. Por supuesto que es posible hacer que el programa use varios hilos, pero sería necesario reescribir y recompilar el programa. Y, de nuevo, este es un ejemplo sencillo. La lección es: la generalidad da más espacio para la optimización.
Una metáfora (espero) adecuada sería pensar en que se tiene un
empleado, que hace cosas por uno. Si le digo "multiplique este par
de matrices", puede que le cueste algo de tiempo mirar cómo se
hace, pero a manera que aparezcan mejores herramientas (una
calculadora, Excel), él podrá hacer la operación más rápido. En
cambio, si le digo "Toma el elemento A[1][1]
y multiplícalo por
B[1][1]
, y luego A[1][2]
y...", él no tendrá opción de acelerar las
cosas, aparte de ejecutar la secuencia de órdenes que le he dado
más rápido.
Un JIT podría darse cuenta de que estoy multiplicando un par de vectores, y usar una instrucción especial del procesador para hacerlo. Se podría argumentar que un compilador también podría darse cuenta y hacer la misma optimización, pero, esto será más fácil de deducir (inducir?) si la operación solicitada es una operación general (haz este producto vectorial), que si se tiene una secuencia de pequeñas instrucciones (haz estos productos, acumúlalos). Y ahí es donde los lenguajes dinámicos hacen la gran diferencia.
Hay varios esfuerzos, y hay evidencia de que sirven. El mejor ejemplo es la JVM. Para Python, está (estuvo) Psyco, y está (estará?) PyPy. Para Lua se tiene LuaJIT. PyPy en particular es muy interesante.
UPDATE: Esta presentación sirvió como inspiración para este post.
Hola,
Generalmente los micro-benchmarks como el que propone son poco ilustrativos de la supuesta "velocidad" de un lenguaje sobre otro.
Para su ejemplo: optimizó el código en C compilándolo con -O3, pero no hizo lo propio con el código en Java. La "velocidad" de un programa en Java es sensible a muchos factores, incluyendo: la versión del JDK que esté usando, la versión y el vendor de la JVM, las opciones de arranque de la JVM que utilice (en particular, se obtiene mejor desempeño con -server), la configuración del JIT, la configuración del Garbage Collector, disponibilidad de memoria en el sistema, al hecho de haber (o no) corrido un par de veces el GC antes de tomar la medida, al hecho de haber dejado (o no) 'calentar' el JIT un rato antes de medir tiempos, y un largo etcétera.
Así que faltó un poco de cacharreo del lado de Java antes de saltar a conclusiones apresuradas.
Saludos,
Trataté de responder a cada comentario puntual. Puede que suene muy cortante, pero por favor no lo tome así, es simplemente ser directo
Hice lo posible para optimizar de la misma forma con Java; eso es lo que intenta la opción -d64 al invocar java. Revisé (por encima) la página man de java, pero no vi ninguna opción relevante (lo que no me sorprende, porque tengo entendido que quien hace el trabajo pesado es HotSpot, no javac)
No tengo idea de cómo configurar el JIT. links?
No creo que el GC importe mucho en este caso (apenas si uso dos long)
Cierto, no tuve en cuenta el tiempo de arranque de la JVM. Que es grande cuando se usa el modo server.
La idea del programa en Java era hacer unas pocas operaciones muchísimas veces, lo que es escenario ideal para el JIT. No me había puesto a pensarlo, pero ahora que ud. lo menciona, el JIT tuvo... 48 segundos, menos el tiempo de arranque de java, para "calentar". Eso debería bastar, creo.
No creo estar saltando a "conclusiones apresuradas". Me imagino que una de ellas es decir que Java es más lento que C. Que no era la idea, porque esto es apenas un microbenchmark de un programa que (imagino que) se puede escribir en muy pocas líneas de assembler, y que pretendía usar para mostrar que, aunque sea más lento en casos especiales como el que muestro, Java (y los lenguajes dinámicos/interpretados) pueden ser tan rápidos como C/C++ en problemas reales (como referencio al enlazar a los benchmarks de http://shootout.alioth.debian.org/)
Igual, mis disculpas, tal vez el punto del post debió manejarse de forma más clara.
Saludos, y bienvenido!
OK, esta buena la discusión
Mi sugerencia para hacer una prueba más 'justa' para Java, sería:
1) arrancar la jvm con -server . HotSpot es más agresivo en sus optimizaciones en este modo 2) correr el código una primera vez, con menos ciclos. Digamos, 10.000 iteraciones, esto le dará tiempo a JIT para hacer su magia 3) sólo por precaución, correr el gc una vez con System.gc() y luego Thread.sleep() digamos, 2 segundos. correr el gc de nuevo, y dormir de nuevo. Si bien es cierto que uno en principio no tiene control sobre el gc, en general la llamada a gc() es atendida, y la razón para llamarlo dos veces es para darle tiempo a liberar objetos recientemente creados; leí eso en alguna parte donde hacían microbenchmarks y en últimas tiene que ver con que el gc es generacional 4) ahora sí, podemos empezar a contar tiempo (aquí ya no vamos a tener en cuenta el tiempo de arranque de la jvm, ni posibles demoras por ejecución del gc, ni el tiempo que pueda tomar hacer compilación JIT), corremos el código de prueba con todas las iteraciones del caso y al terminar tomamos la diferencia de tiempo transcurrida 5) (opcional) si quisiéramos ser más rigurosos, incluso deberíamos hacer más de una medición. Para esto es mejor apoyarse en herramientas como por ej. Java Application Monitor (JAMon)
¿pq tantas vueltas? . Los beneficios de una compilación JIT se cosechan a mediano plazo; un fragmento de código que sólo se llama unas pocas veces no va a verse beneficiado, las ganancias están en el código que se llama con frecuencia y aún así hay que darle tiempo al JIT para que haga lo suyo.
Si hace la prueba con mi receta (y sería interesante), anticipo que va a encontrar que la diferencia en tiempo vs. código en C optimizado es aún menor.
Saludos,
...
yawn
sorry
El punto era mostrar que ese, precisamente, es un ejemplo artifical, y que los lenguajes interpretados (VM) pueden ser tan rápidos como C/C++, en problemas reales, y que tienen el potencial de ser aún más rápidos.
Suit yourself