7 de abril de 2013

Baseline, el nuevo JIT javascript de Firefox (y de paso, la historia de los JITs de Firefox)

Como colofón a la entrada anterior, hoy tengo la oportunidad de retomar la sana costumbre de proselitismo firefoxero, como se hacía en los tiempos del anuncio de dos páginas en el New York Times pagado con donaciones ("crowdfunding" que se diría ahora, en lugar del "fundraising" que se utilizó entonces), o los primeros 100 millones de descargas. Sin intención anti-blink, que conste (aunque quizás la entrada aparentara lo contrario, blink me parece el sucesor más fiel al espíritu del webkit original).

Firefox ha anunciado que acaba de introducir en sus builds nocturnas un nuevo JIT: Baseline. El anuncio es una lectura muy interesante que merece un post épico sobre la intrincada historia de los motores Javascript de Firefox y la evolución de sus JITs.

SpiderMonkey: intérprete a secas

En un principio, el motor Javascript de Firefox (llamado SpiderMonkey desde que Brendan Eich lo escribió mientras creaba el lenguaje) era simplemente un intérprete de bytecode. Por aquel entonces los JITs eran algo reservado a Java y similares. En Firefox 3.5 se introdujo el primer JIT, TraceMonkey. Por aquel entonces tanto el V8 de Chrome como el Squirrelfish de Apple del mundo Webkit ya tenían sus JITs.

TraceMonkey: tracing JIT

Este JIT de Mozilla, sin embargo, era algo diferente. Los motores de webkit eran JITs "whole-method", que quiere decir que compilaban a código nativo métodos enteros. Sin embargo, Javascript es un lenguaje con variables de tipado dinámico, es decir, el tipo de una variable pueden cambiar durante la ejecución, por lo que el tipo de muchas variables no puede determinarse durante la compilación. Por eso, las implementaciones de lenguajes de tipado dinámico tienen que implementar un sistema que vaya tomando nota del tipo de cada variable y pueda enfrentarse a cambios de tipos. Los JITs webkit generaban código nativo que incluía ese sistema, sistema que, como es de esperar, afecta notablemente al rendimiento.

TraceMonkey no generaba código nativo para métodos, lo generaba para "traces", que no son más que una ruta de código determinada. El intérprete empieza a interpretar el bytecode, y cuando el bytecode saltaba "hacia atrás" (caso típico de un loop), tomaba nota de ello. Cuando ese mismo salto ocurría varias veces, TraceMonkey generaba código nativo para esa porción de código.

La gran ventaja de este método era que al entrar en acción no para generar código genérico para una función, sino una ruta de código muy concreta ya ejecutada muchas veces, y conociendo el conjunto de tipos concreto utilizados por las variables en esas ejecuciones, TraceMonkey tenía la oportunidad de optimizar el código nativo para ese conjunto de tipos, sin incluir todo un sistema capaz de funcionar con tipos dinámicos (sólo comprobaciones de cambio de tipo), por lo que el código generado estaba más optimizado que el código de los JITs webkit. Las desventajas era que si una trace cambiaba los tipos asumidos, había que desecharla y volver al intérprete inicial. Y si una ruta de código con muchos "ifs" pasaba siempre por las mismas condiciones y de repente en una cambiaba de dirección, lo mismo.

TraceMonkey era bueno optimizando loops con tipos y rutas de código estables, pero para lo demás se volvía al (lento) intérprete. Los webkit generaban código nativo menos optimizado, por incluir toda la gestión de tipos, pero podían optimizar cualquier función, no una ruta concreta, y su optimización era válida en presencia de cambios de tipos y de rutas de código, y duraba para todo el programa.

JaegerMonkey: whole-method JIT, pero sin eliminar TraceMonkey

Aunque TraceMonkey era bueno en lo suyo, había espacios donde claramente no podía competir en todos los casos con los JITs webkit, así que Firefox 4 introdujo un JIT "whole-method" como los de ellos, llamado JaegerMonkey. Sin embargo, TraceMonkey seguía siendo el mejor para loops estables, por lo que se siguió recurriendo a él para esos casos.

JaegerMonkey + type inference: adios a TraceMonkey

Firefox 9 añadió a SpiderMonkey (el intérprete) inferencia de tipos. A grandes rasgos, la inferencia de tipos consiste en intentar averiguar lo máximo posible sobre los tipos de las variables y sus cambios en todo momento. Gracias a esa información JaegerMonkey pasó a poder hacer optimizaciones de tipos parecidas a las de TraceMonkey.

Dado que JaegerMonkey hacía más o menos el trabajo de TraceMonkey y la integración entre ambos era problemática (y, además, TraceMonkey se había convertido en un monstruo al que se había intentado forzar a hacer lo que no podía), se desactivó TraceMonkey por defecto en Firefox 10, y se eliminó por completo en Firefox 11.

IonMonkey: whole-method JIT + type inference + IR, pero también JaegerMonkey

Firefox 18 (publicado hace medio año) incluyó otro JIT, IonMonkey. Sus fundamentos vienen a ser los mismos que los de JaegerMonkey, excepto en un aspecto. Al compilar una función, JaegerMonkey lo hace traduciendo el bytecode a código máquina nativo más o menos directamente.

IonMonkey no hace esa traducción directa, traduce en primer lugar el código Javascript en primer lugar a unas estructuras de datos, una "representación intermedia" (IR), sobre la cual se pueden aplicar algoritmos de optimización mucho más complejos e infinitamente más eficientes. Es así, de hecho, como funcionan los grandes compiladores. Sólo hay un problema, y es que esta clase de compilación es compleja, lenta y usa muchos recursos. Se trata de la mejor optimización posible, pero sólo merece la pena en métodos que verdaderamente lo merezcan. Por lo que se siguió utilizando JaegerMonkey, que hacía optimizaciones regulares, pero rápidas.

Llegados a este punto, el esquema es:
 · Se empieza con el intérprete SpiderMonkey (que es lento pero es la mejor opción para el código no crítico que casi no se ejecuta, ya que esos casos las compilaciones son más costosas que los beneficios potenciales)
 · Si una función se llama bastante se hace una optimización simple pero rápida con JaegerMonkey.
 · Si esa función sigue llamándose con mucha insistencia, se hace una optimización profunda pero lenta con IonMonkey.
 · Si hay un cambio de tipos en una función en alguno de los dos JITs, se desechan las optimizaciones y se vuelve al intérprete (pero ocurre con menos frecuencia que en TraceMonkey)

Baseline: adiós a JaegerMonkey

El esquema presente de los JITs de Firefox parece equilibrado, pero a través de tanto cambio ha acumulado deficiencias. El encargado de recoger información de los tipos es el intérprete SpiderMonkey, que se lo pasa a los JITs. Es por esa razón que si hay un cambio de tipos es necesario desechar el código nativo generado y volver al (lento) intérprete, para sea él quien recoja la información de los nuevos tipos.

Baseline es un reemplazo de JaegerMonkey, diseñado como él para generar código poco optimizado pero con rapidez, con la diferencia de que es capaz de analizar tipos por si mismo, y gracias a una optimización llamada "inline cache", si hay un cambio de tipos es capaz de adaptarse e incorporarlo a la optimización existente sin empezar de cero. Y toda la información de tipos es compartida con IonMonkey. De ese modo, un cambio de tipos en el código nunca supone el regreso al intérprete, sino a la optimización media de Baseline.

Es más, su gestión de tipos es mucho más avanzada que la del intérprete y permite mejores optimizaciones y consume menos memoria que Jaeger, sin que por ello deje de compilar con rapidez. Además, las llamadas entre código nativo optimizado por Ion y el optimizado por Baseline son más rápidas que con Jaeger. Baseline además comparte mucho código con IonMonkey, y por último, Baseline entrará a optimizar una función cuando se ejecuta 10 veces, mucho antes que JaegerMonkey, que lo hacía tras 40. Los benchmarks muestran una mejora notoria y plena competitividad con otros motores javascript (que, aunque no lo he mencionado, también han ido mejorando con el tiempo).

Y ese es el resumen de la evolución de los motores Javascript de Firefox...

Extra: OdinMonkey (asm.js)

...con una excepción: OdinMonkey, también conocido como asm.js, que es algo reciente. Es un experimento de Mozilla, y no tiene que ver con los motores javascript referidos anteriormente. Se trata de un subconjunto de Javascript legalmente válido, pero que renuncia a las funcionalidades más costosas del lenguaje, como el tipado dinámico o la gestión de memoria automática.

Este subconjunto de Javascript se ha diseñado con la idea -francamente revolucionaria- de permitir compilar código C/C++. Es decir, coger un programa C/C++ y compilarlo no a código x86, sino a este subconjunto de Javascript. Es decir, que este subconjunto de Javascript funcionaría como una especie de "representación intermedia", de pseudobytecode.

Lo gracioso está en que, al prescindir de las características complejas, es posible utilizar no un compilador JIT, sino uno "ahead-of-time", para generar código muy optimizado a partir de ese Javascript especial. Y por muy optimizado estamos hablando que es el doble de lento que los programas generados por compiladores como clang (y eso es ser muy, muy rápido). Ah, y con la seguridad de una máquina virtual.

De momento, asm.js supone la obsolescencia del Native Client de Google Chrome, pero los desarrolladores están planeando facilitar que asm.js pueda ser usado por lenguajes como Java o C#, con lo cual asm.js podría convertirse en una VM universal que compita con las de esos lenguajes.

9 comentarios:

  1. Anónimo3:38 p. m.

    Usted es un maestro. Me permitiría preguntarle cómo es q recaba toda esta información?

    ResponderEliminar
    Respuestas
    1. Google es el maestro. Hay varios posts en internet (y mucha información de Mozilla) que contienen toda esta información, basta buscar y copiarlos^Wecharles un vistazo

      Eliminar
    2. Anónimo4:55 a. m.

      Pero la manera en que redactas esa informacion es excepcional.. me uno a la opinion de otro comentario, que este entrada es simplemente epica.. tanto por el alto nivel tecnico y por lo sencillo que lo explicas...

      Eliminar
  2. Anónimo10:07 a. m.

    Bueno ... yo soy un usuario, si quieres y te sientes generoso me podrías etiquetar como un 'power-user'. Sin embargo y a pesar de ser un fervoroso admirador y consumidor de Firefox y otros productos de Mozilla en general, sin embargo digo, mis conocimientos de Javascript son CERO ... por otra parte mis conocimientos en el idioma inglés son honestamente elevados.


    Bien ... pues a lo largo de estos últimos dos años me habré leido mínimo un par de docenas de artículos (la mayoría en inglés) relatando estas evoluciones en los motores javascript de Firefox. Sinceramente ... ó en los blogposts de turno no se explicaban del todo bien, ó yo estaba 'espeso' ó vete tú a saber qué ... el caso es que nunca terminaba de comprender del todo bien en qué consistían estas evoluciones. Finalmente me quedaba PARCIALMENTE contento porque pensaba que me terminase yo de enterar bien ó no, Firefox evolucionaba para mejor ... que era lo importante.

    En este post que tú ya de entrada has calificado como épico (y yo lo confirmo sin género de dudas) lo he comprendido TODO ... pero todo, todo ... ¡¡¡ qué a gusto me he quedado finalmente !!!

    Este post con ese matiz histórico-didáctico no tiene precio ... tal cual ... es un TESORO. Pero fundamentalmente es que, además estar muy bien explicado, es que en mi opinión, repara en detalles CLAVE que para los no-conocedores de Javascript, es crítico explicitarlo. Por ejemplo, ya te digo que si no explicas el detalle de ... " Javascript es un lenguaje con variables de tipado dinámico, es decir, el tipo de una variable pueden cambiar durante la ejecución" ... mi comprensión del artículo se habría ido desvaneciendo lentamente hasta terminar perdiéndome en algún punto avanzado del post.

    De hecho, tengo la sensación de que me podía haber tirado otro par de años leyéndome artículos similares sacándoles el mismo limitado partido que acostumbraba ... ahora la historia cambiará ....


    Diego ... ¡¡ GRACIAS !!


    Offtopic .- ¿ de verdad prefieres un 'dark theme' en tu blog ?

    ResponderEliminar
    Respuestas
    1. Me agrada que te guste. No sólo prefiero un dark theme, en mi opinión los temas oscuros deberían ser obligatorios.

      Eliminar
  3. Anónimo5:54 p. m.

    los temas oscuros son perfectos para leer texto largo....

    ResponderEliminar
  4. Hola Diego, siempre te leo, aunque no suelo comentar, pero es que este post es como dijiste al inicio: ÉPICO.

    Mil gracias por toda esta información tan bien explicada!

    Saludos :)

    ResponderEliminar
  5. Anónimo9:23 p. m.

    Muy buen articulo, muy claro y completo, gracias...

    Una duda, llegue aqui buscando info sobre el motor de javascript de firefox ya que creo que con el paso de las versiones ha decrecido en velocidad... me explico, hace unos años, en versiones anteriores de firefox, la pagina http://spielzeugz.de/html5/liquid-particles.html en firefox iba muy fluida pero con la version actual y pasadas recientes va muy muy mal, chupando mucha memoria nada de fluidez, imposible...

    pero no puedo descubrir que propiedad es por la que va tan lento...

    no se si sabes mas sobre el tema...

    Un saludo y gracias

    ResponderEliminar
    Respuestas
    1. Ni idea, lo único que te puedo aconsejar es que lo reportes. Aunque he de decir que a mi me funciona con perfecta fluidez.

      Eliminar