Rendimiento de los bucles en Javascript frente a otras alternativas

17 Mar 2011

Hace un par de días ameneció mi cuenta de Twitter con un reto interesante creado originalmente para el lenguaje C y C++: se trataba de crear un programa que imprimiese en pantalla los primeros 1000 números sin utilizar para ello ni bucles ni condicionales.

Rápidamente comenzaron a llegar propuestas muy interesantes que exprimían al máximo las capacidades de estos lenguajes.

Una vez superado el reto, se extendió a otros lenguajes populares como PHP donde, de nuevo, las originales soluciones de los usuarios llevaban al límite sus capacidades. El siguiente paso fue, como no podía ser menos, tratar de resolver el acertijo en Javascript, con lo que de nuevo, tocó estrujarse el coco.

Al final de la mañana, llegamos a un par de soluciones válidas a través de dos puntos de vista parecidos pero con implementaciones diferentes y, de nuevo, llegaba una duda: ¿qué penalización cabía esperar de esas soluciones tan poco convencionales frente al uso de bucles y condicionales naturales?

Vamos a comprobarlo!

Las propuestas

Las dos propuestas que finalmente aceptamos como funcionales son las siguientes:

Mi solución

var y = x = 1000, foo0 = new Function();
(function foo1() {
  console.log( y - x );
  eval( 'foo' + Math.floor( ( --x / y ) + 1 )+ '();' );
}() );

Explicación rápida

Este código declara dos funciones, una donde se ejecuta todo el algoritmo y otra vacía que nos permitirá detener la ejecución.

La idea es que la primera de estas funciones es autoejecutable y en su interior, se construye el nombre de la función que va a ser inmediatamente invocada. Mientras permanezcamos dentro del rango declarado, 1000, el nombre de la función se corresponde con ella misma; cuando salimos de dicho límite, se llama a la función vacía que detiene el ciclo.

Como podemos observar, utilizamos eval para invocar a la función correspondiente preveyendo que este método tiene que ser muy rápido en tiempo de ejecución.

NOTA: No probéis este código directamente en una consola Firebug porque probablemente desbordará la pila de memoria. Se puede modificar el rango de ‘x’ con un valor menor (por ejemplo 25) para comprobar el funcionamiento.

Solución de @TarodBOFH

El desarrollador @TarodBOFH propuso una solución de concepto similar a la anterior pero estructurada mediante un array que actúa como contenedor:

var arr = [
  function(val) {
    console.log(val);
    arr[ Math.floor( val / 1000 ) ]( val + 1 );
  },
  function() {}
];
arr[0](1);

En este caso, no se llama a una función directamente, sino a una posición concreta en un array, lo que hace innecesario el uso de eval dando un código más limpio.

Método estándar

Toca ahora enfrentar a estas dos soluciones con la que utilizaríamos en un entorno real de bucles y condicionales. El código ‘estándar’ sería el siguiente:

for( var x = 1; x < 1001; x++ ){
  console.log( x );
}

Si queremos hacerlo más bonito, se puede reescribir aprovechando al máximo las posibilidades de los bucles Javascript:

for(
  var x = 1;
  x < 101 && console.log( x );
  x++
);

Los resultados del test de comparación

Para medir el rendimiento, se ha utilizado el servicio online JSPerf con los siguientes resultados:

Resultado de los tests de rendimiento JSEn la tabla anterior quedan registrado los tests realizados con los navegadores Chrome 10, Firefox 4 y Opera 11. Traté de realizar las pruebas con IE8 e IE9, pero lanzaban error por exceso de recursión.

Los resultados pintados de verde marcan los tests más rápidos mientras que los rojos los más lentos. El valor obtenido se corresponde con el número de operaciones por segundo lanzadas con cada método.

Revisando estos datos, me ha llamado la atención la clara diferencia de rendimiento entre los métodos en unos resultados que, por otro lado, eran los que cabía esperar. A modo de conclusión, podemos resumir que:

  • Independientemente de la plataforma, el método tradicional es sustancialmente más rápido que los otros.
  • Las funciones recursivas, no importando demasiado la estructura que las implemente, tienen una penalización en rendimiento importante frente a los métodos nativos.
  • El uso de eval resulta el más lento de todos debido a que no guarda el código en caché y debe recompilarlo a cada iteración.

Conclusión

Con este pequeño experimento, hemos comprobado que los bucles naturales de Javascript son mucho más rápidos que otras soluciones alternativas como las funciones autoejecutables y recursivas o las invocaciones a eval.

Para los interesados en ver las soluciones en otros lenguajes de programación, pueden seguir los siguientes enlaces:

Hilo original para C y C++: Printing 1 to 1000 without loop or conditionals
Solución para PHP: Counting to 1000 in PHP without loops or conditionals
Otra solución para PHP: PHP to 1000 without conditionals and loops

Si alguien tiene otra solución alternativa, que no dude en compartirla con los mensajes!

Más:

{2} Comentarios.

  1. Raúl Fraile

    Sería interesante ver cómo se van aproximando/alejando los resultados con números distintos de 1000. Además, me ha sorprendido la velocidad de Opera…

    • Carlos Benítez

      Lo malo es que los entornos de desarrollo más usuales (el navegador básicamente) no suele aguantar iteraciones mayores; por ejemplo, no pude probar en IE porque tiene un límite de 5 millones de instrucciones para secuencias de scripts, por lo que daba error de recursión.

      Probar con límites por encima de 1000 comienza a dar errores y cuelgues también en Firefox. Sin embargo, parece que Opera y Chrome si son capaces de soportarlos.

      Y tienes razón con respecto a la nueva versión de Opera: el motor es realmente rápido!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *