Recuerdo haber leído sobre asm.js, hace como un par de años. Quizás atraído por su nombre, mezclando ensamblador y Javascript. No me pareció nada relevante en su momento, pues además, sólo estaba adoptado en Firefox. Os voy a contar la esencia de asm.js, y vosotros decidiréis.

asm.js es un subconjunto de Javascript, es decir, cualquier navegador que ejecute Javascript, es en principio capaz de ejecutar asm.js. Está claro, aunque de momento no se le vea la utilidad.

Pero si desde hace años, que Javascript ya no es interpretado, y utiliza un compilador JIT, que en tiempo de ejecución convierte el script a código nativo, ¿qué ganamos? Bueno, digamos que al ser un subconjunto del lenguaje, permite que las instrucciones soportadas, puedan analizarse mejor, y por tanto optimizarse el rendimiento de las mismas. En este caso, las optimizaciones extras, si que requieren que el navegador lo soporte. Es decir, si el navegador acepta asm.js, correremos a velocidad extra, mientras que si no, lo haremos a velocidad habitual de Javascript, en realidad, un pelín más despacio, como luego veremos.

asm.js, utilizando construcciones ya existentes en Javascript, palía dos problemas del lenguaje en cuanto a rendimiento. El primero es que Javascript sigue siendo un lenguaje sin tipos de datos. En general, cualquier variable puede almacenar cualquier tipo de dato. ¿Os suenan los variants de Microsoft? Se lleva hablando más de una década sobre una nueva revisión de Javascript con soporte opcional al tipado, como ocurrió entre ActionScript 2 y ActionScript 3. Sería una gran ventaja en cuanto a la reducción de errores, y a la mejora del rendimiento. Pero quizás Javascript dejaría de tener la imagen de lenguaje sencillo y limpio que tiene. Un aspecto que por otro lado, es cada vez menos cierto.

Ya tenemos la primera mejora, en asm.js podemos emular tipos de datos. De manera que si antes escribíamos:

var iCont = 0

En asm.js escribimos algo como:

"use asm";
var iCont = 0|0

El indicativo de use asm, actúa de manera similar al use strict, indicando al navegador, que lo siguiente, debería ser ejecutado como asm.js. La idea es la misma, es un literal que se ignora si no está soportado, así que no se rompe nada. El pipe, es el operador lógico de OR de Javascript de toda la vida. A efectos prácticos no hace nada, cualquier valor, al que se le ejecuta una O lógica sobre cero, sigue resultando el valor original. Pero, ayuda a asm.js a saber que iCont, sólo tendrá valores enteros. Es decir, forzamos el tipado. Esto ya nos da una pista, ahora si, que ese código es mucho más sencillo de utilizar. En cuánto a porque no haberse aprovechado de las anotacionea, mucho más legibles, es algo que desconozco.

El segundo aspecto que eliminamos es la gestión de memoria. Javascript, a diferencia de C++ por ejemplo, tiene un recolector de basura (GC), se encarga de limpiar aquellos recursos que no usemos, sin que el programador tenga que indicarlo explícitamente. Es un mecanismo muy cómodo, pero que causa bastante penalización en cuanto al rendimiento, y también en la consistencia. Un fragmento de tu programa puede ejecutarse una vez en 0,1 segundos, y la siguiente 0,2 segundos si en ese momento el GC estaba actuando en segundo plano. Sin garbage colector, el soporte de asm.js, es también más fácil.

Pues bien, ya hemos conseguido convertir Javascript, en un lenguaje tipado, y sin recolector de basura. Sobre esa base, se puede implementar en el navegador, un mecanismo que denominan AOT (Ahead Of Time). En contraposición al JIT, ese AOT, lo que hace es cuando encuentra código asm.js, convertirlo directamente a código máquina. Podríamos decir, que directamente coge ese Javascript limitado, y lo convierte a ensamblador, de ahí lo de asm.js.

¿Pero eso no es lo que hace el JIT? En efecto, el JIT de Javascript, también coge el código de script, y lo convierte a código nativo. Lo que pasa es que es un trabajo mucho más complicado, pues debe estudiuar flujos de ejecución, discernir tipos de variables, aplicar optimizaciones, y recompilar dinámicamente. asm.js, en principio no optimiza, simplemente transforma.

Se dice que con asm.js, el rendimiento es entorno a la mitad del código escrito en C, y que podemos obtener mejoras de hasta 10x comparado con Javascript normal. Pero lo habitual es entre 1,5x y 3x.

Todo esto, parece demasiado complicado para lo que en realidad queremos. ¿No hubiera sido más fácil poder prescindir del GC y añadir tipado? Pues soy de la opinión que sí, pero esos cambios, aunque mejores, habrían requerido del soporte de los navegadores, y desarrolladores. Y ya vemos que asm.js, puede ejecutarse aunque el navegador no esté optimizado para él.

Lo siguiente, viene relacionado a asm.js, y explica gran parte de su éxito. Tenemos Emscripten, una herramienta que convierte código objeto de CLang a asm.js. Virtualmente puedo generar código Javascript de mis programas en C++, con mayor o menor esfuerzo. Así vemos motores 3D, o librerías como Qt, que han seguido este camino, y ahora funcionan en un navegador.

Decíamos que asm.js no optimiza el código. Ciertamente, con Emscripten, partimos que ha sido el compilador de C/C++, quien ha aplicado esas optimizaciones previamente. Sabemos que un compilador de C maduro, generará un código de excelente calidad. Un compilador de C, puede tomarse minutos, o incluso horas en generar el código de un proyecto, analizar estáticamente los flujos de ejecución, o incluso utilizar instrumentación PGO para conseguirlo. En cambio el JIT de Javascript, apenas dispone de unas décimas de segundo, pocos segundos en el mejor de los casos. Recordemos que compilar se hace sólo una vez. En cambio la compilación JIT, se hace cada vez que se carga una página… ¡Y el usuario está esperando para verla! Por tanto los resultados no pueden ser los mismos.

Librerías como pdf.js o bpgdec, se aprovechan de asm.js para obtener unos rendimientos cercanos al código nativo. A día de hoy, sólamente unos pocos navegadores tienen caminos optimizados para asm.js (Firefox 22 y Edge 13). Otros lo soportan, pero sin implementar AOT (Chrome 22, y Opera 15), y finalmente en otros es experimental y viene desactivado por defecto (IE 11).

La idea de asm.js, no es la de escribir código directamente en ese dialecto, sino utilizar conversores/compiladores para hacerlo. En lo que a mi respecta, admito que el código asm.js es poco legible comparado con Javascript, y al mismo tiempo limitado. Sin embargo, en códigos que requieren un cálculo intensivo, considero que puede valer la pena trabajar directamente con asm.js. Un poco como antaño se usaba el ensamblador inline en programas BASIC, Pascal o C. Una vez tenemos nuestro código escrito, podemos usar las herramientas de depuración habituales de Javascript. Basta con eliminar la directiva “use asm” y poderlo ejecutar y trazar, como si fuera un programa Javascript normal.