Asm.js: Compilando a javascript

El contenido de este post no es mío. Es una traducción (libre) que me tome la libertad de hacer de lo que escribió  Jhon Resig en su blog acerca de Asm.js.El articulo me pareció muy interesante y enriquecedor y creí que seria muy bueno que este disponible en castellano, especialmente para aquellos que todavía no están convencidos del potencial de JavaScript. De más esta decir que recomiendo completamente que lean el articulo original en lugar de este si pueden leer ingles sin problemas.

El árticulo continene una muy buena explicación de como funciona asm.js, ademas de una estupenda entrevista a David Herman contandonos un poco los objetivos y lo que podemos esperar de asm.js en el futuro. Sin más, el articulo:

Link al árticulo original: http://ejohn.org/blog/asmjs-javascript-compile-target

Igual que muchos desarrolladores, estaba muy entusiasmado con lo que promete Asm.js. Enterarme que Asm.js esta disponible en Firefox Nightly hizo que mi interés aumente. Mucha gente también lo hizo, de hecho, cuando Mozilla y Epic anunciaron (mirror) que habían portado el motor Unreal 3 – y que funcionaba muy bien.

Hacer que el motor de un juego escrito en C++ corra en javascript, usando WebGL para sus gráficos, es un logro realmente increíble y se debe en parte al conjunto de herramientas desarrolladas por la gente de Mozilla.

Desde la salida del motor de Unreal 3 en javascript, comencé a seguir la recepción de esta noticia por twitter, blogs, etc y ademas de ver que muchos desarrolladores comenzaron a mostrar interés por las tecnologías abiertas que hicieron esto posible, también ví mucha confusión: ¿Asm.js es un plugin? ¿Mi código javascript sera más rápido si uso Asm.js? ¿Funciona en todos los browsers? Siento que Asm.js y las otras tecnologías relacionadas, son muy importantes y me gustaría explicar como funciona, así los desarrolladores saben que es lo que esta pasando y como se verán beneficiados.

¿Que es Asm.js?

Para entender Asm.js y donde se ubica dentro del browser, es necesario saber de donde viene y para que fue creado.

Asm.js viene de un nuevo tipo de aplicación js: aplicaciones C/C++ que fueron compiladas en JavaScript. Es un nuevo tipo de aplicaciones que surgieron a partir del proyecto de Mozilla Emscripten.

Emscripten toma código C/C++, lo pasa a través de LLVM y convierte el bytecode generado a JavaScript. (Asm.js es, concretamente, un subset de JavaScript).

Si el código Asm.js compilado hace algún tipo de renderizado, entonces seguramente sera manejado por WebGL (usando OpenGL para ello). De esta manera, todo el trabajo es llevado a cabo haciendo uso del browser y JavaScript pero esquivando, prácticamente, todo el camino normal de ejecución y renderizado que tienen las aplicaciones Javascript-en-el-browser normales..

Asm.js es un subset de JavaScript altamente limitado en cuanto a lo que puede hacer y sobre lo que puede operar. De esta forma, el código compilado a Asm.js puede ejecutarse rápidamente haciendo las menores suposiciones posibles, convirtiendo el código JavaScript directamente a ensamblador. Es importante tener en cuenta que Asm.js es simplemente JavaScript – Osea, no necesitamos ningún plugin especial para ejecutarlo en el browser (Aunque un browser capaz de detectar el código compilado en Asm.js puede hacerlo mucho más rápido). Es un subset especial de JavaScript optimizado para que sea más “performante”, especialmente cuando se trata de código compilado a JavaScript.

La mejor forma de entender como funciona, es mirar un poco el código compilado a Asm.js. Vamos a ver una función extraída de una aplicación real compilada a Asm.js (De la demostración BananaBread). Le dí un poco de formato a este código para que sea más fácil de digerir, pero normalmente es un montón de código JavaScript minificado.

Técnicamente, es código JavaScript, pero podemos ver que difiere bastante de lo que estamos acostumbrados a ver en aplicaciones normales que hacen uso del DOM.

Algunas cosas de las que podemos darnos cuentas simplemente mirando este código:

  • Este código en particular, solo trabaja con números. De hecho, todo el código compilado a Asm.js hace esto. Asm.js solo puede trabajar con un grupo selecto de tipos numéricos y ninguna otra estructura de datos (Esto incluye strings, booleanos y objetos).
  • Todo dato externo, es almacenado y referenciado desde un único objeto, denominado pila. Esencialmente, esta pila es un array gigante (Que intenta ser un array tipado,  altamente optimizado para mejorar su rendimiento). Todos los datos son almacenados dentro de este array reemplazando efectivamente a las variables globales, estructuras de datos, closures y cualquier otra forma de almacenamiento de datos.
  • Cuando se accede o se asigna algún dato, su valor es forzado a un tipo especifico, siempre. Por ejemplo f = e | 0; asigna a la variable f el valor de e pero también asegura que este valor va a ser un entero (| 0 justamente, convierte un valor a entero). También vemos esto con los números flotantes – fijate el uso de 0.0 y g[...] = +(...);.
  • Mirando los valores que entran y sale de las estructuras de datos, pareciera como si el dato estructurado representado por la variable c es un Int32Array (almacenando enteros de 32 bits. Los valores son siempre convertidos hacia o desde un entero usando| 0) y g es un Float32Array (almacenando flotantes de 32 bits. Los valores son siempre convertidos a un flotante encerrando el valor con +(...)).

Haciendo esto, el resultado esta altamente optimizado y puede ser convertido directamente desde esta sintaxis Asm.js hacia ensamblador sin tener que interpretarlo como tendría que hacerlo con JavaScript. Esto recorta muchas cosas que pueden hacer que un lenguaje dinámico, como Javascript, sea lento: Como la necesidad de un recolector de basura y tipos dinámicos.

Como ejemplo de un código Asm.js más “explicativo” vamos a ver uno sacado de la especificación de Asm.js:

function DiagModule(stdlib, foreign, heap) {
    "use asm";

    // Variable Declarations
    var sqrt = stdlib.Math.sqrt;

    // Function Declarations
    function square(x) {
        x = +x;
        return +(x*x);
    }

    function diag(x, y) {
        x = +x;
        y = +y;
        return +sqrt(square(x) + square(y));
    }

    return { diag: diag };
}

Este código es un poco más claro y podemos ver mejor como funciona Asm.js. Cada modulo está contenido dentro de una función y comienza con la directiva"use asm";. Esto le dice al interprete, que todo lo que se encuentra dentro de esta función debe ser tratado como Asm.js y ser compilado directamente a ensamblador.

Hay que prestar atención a los parámetros de la función: stdlib, foreign, and heap. El objeto stdLib tiene referencias a varias funciones matemáticas nativas. foreign proveé funcionalidades definidas por el usuario (Como podría ser, dibujar una figura en WebGL). Por último heap es un ArrayBuffer que puede ser visto, según el caso, como distintos tipos tales como Int32Array y Float32Array.

El resto se divide en tres partes: declaración de variables, declaración de funciones y por último, un objeto para exportar las funciones que serán expuestas al usuario.

La última parte, la que exporta las funciones, es muy importante, ya que permite que todo el código dentro del modulo pueda ser tratado como Asm.js y a la vez ser usado por código JavaScript normal.

De esta forma, podrías tener algo así usando el mencionado DiagModule:

document.body.onclick = function() {
    function DiagModule(stdlib){"use asm"; ... return { ... };}

    var diag = DiagModule({ Math: Math }).diag;
    alert(diag(10, 100));
};

Esto nos daría a DiagModule en Asm.js que sera tratado de manera especial por el interprete de JavaScript pero aún así estará disponible para otro código JavaScript (Esto quiere decir que podremos usarlo dentro de un evento onClick, por ejemplo)

¿Que tal es el rendimiento?

Actualmente, la única implementación que existe es en las versiones nightly de Firefox (y solo para un par de plataformas). Dicho esto, las primeras pruebas mostraron resultados muy, muy, buenos. En aplicaciones complejas (como los juegos mencionados) el rendimiento es solo 2 veces menor que el del código nativo compilado desde C++ (Lo que es comparable a otros lenguajes como Java o C#). Esto es enormemente más rápido que lo que logran los browsers actualmente, alrededor de 4 a 10 veces más que las ultimas versiones de Firefox y Chrome.

Esto es una enorme mejora sobre lo que tenemos actualmente. Considerando lo temprano de su desarrollo, podemos incluso pensar un rendimiento mucho mejor en las próximas releases de Asm.js

Es interesante ver la enorme diferencia entre Asm.js y los motores actuales en Firefox y Chrome. Una mejora de rendimiento de 4 a 10 veces es enorme (Es como comparar a los browsers actuales con el rendimiento de IE6).
Algo interesante, es que incluso con esta diferencia de rendimiento, muchos de los demos en Asm.js funcionan bien en las versiones actuales de Chrome y Firefox, lo que nos da un buen indicador del estado actual de los motores JavaScript. Sin embargo, su rendimiento no es tan bueno como el ofrecido por un browser capaz de optimizar código Asm.js.

Casos de uso

Hay que tener en cuenta que casi todas las aplicaciones que se usan con Asm.js actualmente son aplicaciones C/C++ compiladas usando Emscripten. Teniendo esto en cuenta, el tipo de aplicaciones que van a compilar a Asm.js, en el futuro cercano, son aquellas que se pueden beneficiar de la portabilidad de un browser, pero que son muy complejas como para ser hechas directamente en JavaScript.

Hasta ahora, la mayoría de los casos de uso se centraron en código donde el rendimiento es muy importante: Tales como juegos, manejo de gráficos, interpretes de lenguajes de programación y bibliotecas. Un vistazo rápido en la lista de proyectos de Emscripten muestra muchos proyectos que serán de uso instantáneo para muchos desarrolladores.

Soporte para Asm.js

Como vimos al principio la versión nightly de Firefox es el único browser que soporta la optimización de Asm.js.

Sin embargo, es importante destacar que el código formateado para Asm.js es ni más ni menos que código Javascript, con un importante set de restricciones. Por esta razón, el código compilado a Asm.js puede correr en otros browsers al igual que cualquier otro código JavaScript, incluso si el browser no lo soporta.

La parte crítica del rompecabezas es el rendimiento de ese código: Si el browser no soporta arrays tipados o no compila especialmente el código Asm.js el rendimiento sera mucho menor.

Asm.js y el desarrollo web

Como seguramente te habrás dado cuenta, el código Asm.js no esta pensado para que lo escribamos a mano. Sera necesario alguna herramienta para escribirlo y van a ser necesarios algunos cambios en como escribiríamos código JavaScript normal para poder usarlo. El caso de uso más común para Asm.js en este momento es con aplicaciones compiladas desde C/C++ a JavaScript. Casi ninguna de estas aplicaciones interactuan con el DOM de forma significativa, mas allá de usar WebGL y demas.

Para que esto pueda ser usado por desarrolladores normales es necesario tener lenguajes más “accesibles” que puedan compilar a Asm.js. El mejor candidato, hasta el momento, es LLJS que esta comenzando a lograr compilar hacia Asm.js. Es importante notar que un lenguaje como LLJS va a seguir siendo un poco diferente de lo que seria un código “normal” escrito en Javascript y seguramente confundira a muchos usuarios de JavaScript. Incluso con un lenguaje más “amigable” cómo LLJS seguramente seguirá siendo usando únicamente por desarrolladores avanzados que necesitan optimizar al máximo su código.

Incluso con LLJS, o cualquier otro lenguaje, que pueda permitir escribir código Asm.js “a mano” no habría un DOM igualmente optimizado sobre el cual trabajar. El entorno ideal seria uno donde podamos compilar LLJS y el DOM juntos para crear un único “bloque” en  Asm.js para que corran de forma simultanea. No estoy seguro cual seria el rendimiento de algo así, pero ¡Me encantaría verlo!

Preguntas y respuestas con David Herman

Le mandé algunas preguntas a David Herman (Investigador senior en Mozilla Research) para intentar obtener un poco más de claridad en como funcionan juntas todas estas piezas de Asm.js y como deberíamos esperar beneficiarnos de ellas. Amablemente se tomo el tiempo de responderme en profundidad y darme respuestas excelentes. Espero que las encuentren tan iluminadoras como yo.

¿Cual es el objetivo de Asm.js? ¿A que publico apunta?

Nuestro objetivo es hacer que la web abierta sea una máquina virtual completa, sobre la cual compilar otros lenguajes y plataformas. En esta primer realease, estamos enfocados en compilar código de bajo nivel como C y C++. Más adelante esperamos darle soporte a constructores de más alto nivel como objectos estructurados y recolectores de basura. De esta forma nos gustaría soportar aplicaciones de otras plataformas como la máquina virtual de Java o .NET.

Ya que asm.js esta pensado, en realidad, para expandir las bases de la web, hay un rango muy amplio de público potencial. Pensamos que un grupo que pueden sacar gran provecho de esto son los desarrolladores de juegos que necesitan acceder a todo el poder computacional que puedan. Pero los desarrolladores web son ingeniosos y siempre encuentran formas de usar todas las herramientas que tienen disponible en formas que no habíamos planeado, así que lo que más deseo es que asm.js sea una tecnología que permita todo tipo de aplicaciones innovadoras que no puedo imaginar actualmente.

¿Tiene sentido crear una versión más “amigable” de Asm.js, como una versión actualizada de LLJS? ¿Se planea expandir el proyecto a ser algo más que algo sobre lo cual compilar?

Totalmente. De hecho, mi colega James Long anuncio recientemente que hizo un fork de LLJS que compila a asm.js. Mi equipo en Mozilla Research quiere incorporar el trabajo de James y hacer que LLJS ofrezca soporte oficial para asm.js.

En mi oponion, normalmente vas a querer escribir asm.js a mano en un par de casos muy limitados, como en cualquier lenguaje ensamblador. Normalmente, vas a querer usar lenguajes más expresivos que compilen eficientemente a él. Por supuesto, cuando los lenguajes, como JavaScript, se vuelven muy expresivos, perdes predictivilidad sobre el rendimiento. (Mi amigo Slava Egorov escribio un post muy bueno explicando los retos de escribir código de alto rendimiento en lenguajes de alto nivel) LLJS esta pensado para un puesto intermedio, como un ensamble de C a asm.js que es más facil de escribir que código asm.js crúdo pero que es más predecible en cuanto a optimización que el código JS normal. Pero a diferencia de C, tiene una buena interoperatibilidad con el JS común. De forma que puedas escribir la mayor parte de tu aplicación en código de forma más flexible en JS y concentrarte en escribir solamente las partes más criticas en LLJS.


Hay un nuevo debate entre los browsers que soportan Asm.js y los que no, similar al que ocurrió durante la última carrera sobre rendimiento de JavaScript en 2008/2009. A pesar de que técnicamente el código Asm.js pueda ejecutarse en cualquier lugar, la diferencia de rendimiento va a ser demasiado grande en muchos casos. Dada esta división, ¿por que elijieron compilar a JavaScript? ¿Por que JavaScript en vez de un lenguaje especial o un plugin?

Para empezar, no creo que la división sea tan horrible como vos la menciones: Creamos demos sorprendentes que funcionan muy bien en navegadores actuales pero que se benefician enormemente usando la optimización ofrecida por asm.js

Es cierto que podes crear aplicaciones que van a depender completamente de la fuerte optimizacion de asm.js para que sean usbales. Al mismo tiempo, como cualquier feature nueva de la plataforma web, las aplicaciones pueden decidir tener un comportamiento distinto con algún fallback menos “costoso”. Hay una gran diferencia en aquellas aplicaciones que funcionan con un rendimiento diferente que aquellas que directamente no funcionan.

Tene en cuenta que la carrera por el rendimiento en los browsers que comenzo a finales de la decada del 2000 fue muy buena para la web y las aplicaciones evolucionaron mucho junto con los browsers. Creo que va a ocurrir lo mismo con asm.js.


¿Cómo compararias Asm.js con el Cliente Nativo de Google? Parece tener objetivos similares a pesar de que Asm.js tiene la ventaja de “simplemente funcionar” en cualquier lugar que pueda correr JavaSCript. ¿Han hecho comparaciones en cuanto a rendimiento?

Bueno, el Cliente Nativo es un poco diferente, ya que implica distribuir código ensamlador especifico de cada plataforma; No creo que Google haya pensado en eso como una tecnologia para contenido web (Si no más bien para contenido disponible en el Web Store de Chrome o extensiones de Chrome), o al menos no en el último tiempo.

El Cliente Nativo Portable (PNaCl, Portable Native Client) tiene un objetivo más similar, usando bitcode LLVM independiente de la plataforma en vez de ensamblador puro. Como dijiste, la primer ventaja de asm.js es la compatibilidad con los browsers existentes. Incluso evitamos tener que crear una sistema de interfaz y repetir el trabajo ya realizado para  las web API como lo hace la Pepper API, ya que asm.js tiene acceso a la API existente usando JavaScript directamente. Por último, tenemos la ventaja de que es muy facil de  implementar: Luke Wagner tuvo nuestra primera implementacion de OdinMonkey en Firefox en tan solo un par de meses, trabajando mayormente solo. Dado que asm.js no tiene un gran numero de llamadas al sistema y API y porque esta hecho usando la sintaxis de JavaScript, podes reusar un monton de lo que ya existe para los motores JavaScript y el runtime de la web.

Podemos hacer comparaciones de rendimiento con PNaCl pero llevaria algo de trabajo y estamos más enfocados en achicar la brecha que existe con el rendimiento nativo. Esperamos poder configurar ciertos benchmarks automaticos así podemos mostrar nuestro progreso comparado con los compiladores nativos de C/C++.


Emscripten, otro proyecto de Mozilla, parece ser el principal productor de código compatible con Asm.js. ¿Cuanto de Asm.js es conducido por las necesidades del proyecto Emscripten? ¿Que beneficios obtuvo Emscripten ahora que las mejoras fueron hechas al nivel del motor?

Usamos Emscripten como nuestro primer test case para asm.js como una forma de asegurarnos que tiene lo necesario para ser usado por aplicaciones nativas reales. Y por supuesto que que los beneficios de Emscripten benefician a cualquiera que tenga aplicaciones nativas y quieran portarlas tal como Epic Games, con quienes nos unimos para portar el engine Unreal 3 a la web en tan solo un par de días usando Emscripten y asm.js.

Pero asm.js puede beneficiar a cualquiera que quiera compilar a un conjunto de bajo-nivel de JavaScript. Por ejemplo, hemos hablado con los muchachos que construyeron el compilador Mandreel, que funciona similar a como lo hace Emscripten. Creemos que se pueden beneficiar al compilar a am.js como empezo haciendolo Emscripten.

Alon Zakai Estubo recompilando benchmarks que generalmente corren 2 veces más lentos que los nativos, donde antes veiamos resultados de 5 a 10 o 20 veces. Esto tan solo en nuestra primer release de OdinMonkey, el backend para asm.js del motor JavaScript SpiderMonkey de Mozilla. Espero ver más mejoras en los próximos meses.


¿Que tan fluida es la especificación de Asm.js? ¿Estan dispuestos a agregar features adicionales (como ser, estructuas de dato más avanzadas) a medida que más autores de compiladores apunten a el?

Podes estar seguro. Luke Wagner escribio un roadmap para asm.js y OdinMonkey en la wiki de Mozilla, en donde se discute algunos de los próximos planes. Tengo que decirte que ninguna es fija todavía, pero te puede dar una idea sobre lo que vamos a trabajar más adelante. Estoy muy emocionado por agregar soporte para los objetos estructurados de ES6. Esto podria proveer recoleción de basura ademas de estructuras de datos tipadas que podrian ayudar a compiladores como JSIL que compilan a lenguajes como C# o Java a JavaScript. Tambien esperamos usar algunos de los tipos de valores propuestos para ES7 para proveer soporte para numeros flotantes de 32 bits, enteros de 64 bits e incluso vectores de longitud fija para soportar SIMD.

¿Sera posible, o práctico, tener un transcompilador JavaScript-a-Asm.js?

Seguramente, sí, pero ¿práctico? No estoy seguro. ¿Te acordas en Inception, cada vez que metes un sueño-dentro-de-un-sueño, el tiempo se hacia más lento? Seguramente pasará lo mismo cada vez que quieras intentar correr un motor de JS dentro de sí mismo. Haciendo un calculo rapido, si asm.js ejecuta código nativo a la mitad del tiempo normal, entonces ejecutar un motor de JS en asm.js va a ejecutar código a la mitad de la velocidad normal de ese motor.

Claro que, vas a poder intentar ejecutar un motor de JS en otro distinto y ¿Quien sabe? el rendimiento en realidad nunca es algo tan claro como lo es en la teoria. ¡Agradeceria que algunos hackers intrepidos lo prueben! De hecho, Alex Tatiyants, estudiante de Stanford, ya compilo el motor SpiderMonkey de Mozilla a JS usando Emscripten; Todo lo que tenes que hacer es usar los flags para el compilador de Emscripten para generar asm.js. Alguien con más tiempo disponible deberia intentarlo.


Actualmente, todo el código del DOM o especifico del browser se maneja por fuera de Asm.js. ¿Hay intenciones de hacer una versión del DOM Emscripten-a-Asm.js-compilado (Algo así como DOM.js)?

Esta es una idea estupenda. Quizá sea un poco complicado con la versión preliminar de asm.js que no tiene soporte para objetos. ¡A medida que hagamos crecer asm.js para incluir soporte para los objetos tipados de ES6 algo como esto puede ser feasible y bastante eficiente!

Una buena aplicación de esto puede ser ver que tanto de la plataforma web puede ser autocontenida con un buen rendimiento. Una de las motivaciones detras de DOM.js fue ver si una implementación del DOM escrita puramente en JS puede resolver el problema tracional del manejo de referencias y de memoria entre la pila de JS y el conteo de referencias de los objetos DOM de C++. Con el soporte a asm.js, DOM.js puede obtener estas mejoras de rendimientos más los beneficios de estructuras de dato más optimizadas. Es algo que vale la pena investigar.


Dado que es bastante dificil de escribir Asm.js, en comparación con código JavaScript normal, ¿que tipo de herramientas les gustaria tener para poder ayudar a desarrolladores o autores de compiladores?

Lo primero y principal, necesitamos lenguajes como LLJS, como ya mencionaste, que compilen a asm.js. Y tendremos algunos de los retos comunes de compilar a la web, como mapear el código generado de nuevo al código original en las herramientas para desarrolladores del browser, usando tecnologias como source maps. Me encantaria ver que se mejoren los source maps para incorporar más información de debugging, aunque seguramente habrá una relación costo/beneficio a la que ajustarse entre la información minima que se obtiene de los source maps y la información super compleja de los formatos de metadatos tales como DWARF.

Para asm.js, me parece que me voy a concentrar en LLJS por ahora, pero siempre agradesco ideas de los desarrolladores sobre como mejorar su experiencia.


Supongo que estan abiertos a trabajar con otros fabricantes de browsers ¿Que discución o colaboración a habido hasta ahora?

Por supuesto. Hemos tenido un par de discuciones informales y se han mostrado predispuestos, seguramente tendremos más. Soy muy optimista en que podamos trabajar con varios fabricantes de browsers para tener asm.js en cualquier lugar que podamos implementarlo verdaderamente sin mucho esfuerzo o cambios de arquitectura. Como dije, el hecho de que Luke haya podido implementar OdinMonkey en cuestion de unos pocos meses es muy alentador. Estaria feliz de ver un bug para soportar asm.js en V8.

Pero más interesa que los desarrolladores puedan hechar un vistazo a asm.js y ver que opinan y nos provean su feedback, tanto a nosotros como a otros fabricantes.

  • Sebastián Seba

    LLVM está con todo. Ayer descubrí Numba que compila código Python a LLVM. Estuve haciendo unos benchmarks y ¡whow!