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.