WebAssembly, the journey – JIT Compiladores

Esta es la segunda parte de una serie de publicaciones sobre nuestro viaje en WebAssembly. Si está comenzando con este artículo, es posible que desee comenzar allí .

En el último artículo , se expuso nuestra motivación y nuestro PoC para medir WebAssembly junto con una explicación de nuestra implementación en Vanilla JS. Para continuar este viaje y entender por qué WebAssembly es, en teoría, más rápido que JavaScript, debemos entender primero un poco del historial de JavaScript y lo que lo hace tan rápido hoy en día.

Un poco de historia

JavaScript fue creado en 1995 por Brendan Eich con el objetivo de ser un lenguaje que los diseñadores puedan implementar fácilmente interfaces dinámicas,
en otras palabras, no fue construido para ser rápido; fue creado para agregar capas de comportamiento en páginas HTML de manera cómoda y directa.

Cuando se introdujo JavaScript, así era como se veía Internet.

Inicialmente, JavaScript era un lenguaje interpretado, lo que hace que la fase de inicio sea más rápida porque el intérprete solo necesita leer la primera instrucción, traducirla a bytecode y ejecutarla de inmediato. Para las necesidades de Internet de los 90, JavaScript hizo su trabajo muy bien. El problema radica en que las aplicaciones comienzan a ser más complejas.

En la década de 2000, tecnologías como Ajax hicieron que las aplicaciones web fueran más dinámicas, Gmail en 2004 y Google Maps en 2005 eran una tendencia en este caso de uso de la tecnología Ajax. Esta nueva "forma" de crear aplicaciones web termina con más lógica escrita en el lado del cliente. En este momento, JavaScript tuvo que dar un salto en su rendimiento, que ocurrió en 2008 con la aparición de Google y su motor V8 que compiló todo el código JavaScript en bytecode de inmediato. Pero, ¿cómo funcionan los compiladores JIT?

¿Cómo funciona JavaScript JIT?

En resumen, después de cargar el código JavaScript, el código fuente se transforma en una representación en árbol llamada Árbol de sintaxis abstracta o AST. Después, dependiendo del motor / sistema operacional / plataforma, se compila una versión de referencia de este código, o se crea un bytecode para ser interpretado.

El Analizador es otra entidad que debe observarse, que supervisa y recopila datos de ejecución de código. Lo describiré en resumen cómo funciona, teniendo en cuenta que son diferencias entre los motores de los navegadores.

En el primer momento, todo pasa por el intérprete; este proceso garantiza que el código se ejecuta más rápido después de que se genera AST. Cuando un fragmento de código se ejecuta varias veces, como nuestra función getNextState() , el intérprete pierde su rendimiento ya que necesita interpretar el mismo fragmento de código una y otra vez, cuando esto sucede, el perfilador marca este fragmento de código como cálido y el compilador de línea base entra en acción.

Compilador de línea de base

Para ilustrar mejor cómo funciona JIT, de ahora en adelante vamos a usar el siguiente fragmento como ejemplo.

 función suma (x, y) { 
devolver x + y;
}
 [1, 2, 3, 4, 5, '6', 7, 8, 9, 10] .reduce ( 
(anterior, curr) => suma (anterior, curr),
0
);

Cuando Profiler marca un fragmento de código como cálido, el JIT envía este código al compilador de línea base, que crea un stub para esta parte del código mientras el generador de perfiles sigue recopilando datos con respecto a la frecuencia y los tipos utilizados en esta sección del código (entre otros datos) . Cuando se ejecuta esta sección de código (en nuestro ejemplo hipotético return x + y; ), el JIT solo necesita tomar esta pieza compilada nuevamente. Cuando se llama un código cálido varias veces de la misma manera (como los mismos tipos), se marca como caliente .

Compilador optimizador

Cuando un fragmento de código está marcado como caliente, el compilador del optimizador genera una versión aún más rápida de este código. Solo es posible en función de algunas suposiciones que hace el compilador del optimizador, como el tipo de las variables o la forma de los objetos utilizados en este código. En nuestro ejemplo, podemos decir que un código de return x + y; caliente return x + y; supondrá que tanto x como y se escriben como un number .

El problema es cuando este código ha sido golpeado con algo no esperado por este compilador optimizado, en nuestro caso la llamada a la sum(15, '6') , ya que y es una string . Cuando esto sucede, el Analizador asume que sus suposiciones eran incorrectas, descartando todo volviendo a la versión compilada (o interpretada) de base nuevamente. Esta fase se llama desoptimización . A veces, esto sucede tan a menudo que hace que la versión optimizada sea más lenta que utilizando el código base compilado.

Algunos motores de JavaScript tienen un límite con respecto a la cantidad de intentos de optimización, y dejan de tratar de optimizar el código cuando se alcanza este límite. Otros, como V8, tienen heurísticas que evitan que el código se optimice cuando sabe que lo más probable es que se desoptimice. Este proceso se llama rescate .

Entonces, en resumen, las fases del compilador JIT podrían describirse como:

  • Analizar gramaticalmente
  • Compilar
  • Optimizar / desoptimizar
  • Ejecución
  • Recolector de basura

Ejemplo de fases JIT en V8 por Addy Osmani

Todos estos avances traídos por el compilador JIT hacen que Javascript sea más rápido que 2008 antes de su llegada a Google Chrome, hoy en día las aplicaciones son más robustas y sofisticadas gracias a la velocidad encontrada en los motores JavaScript, pero lo que nos hará tener el mismo rendimiento que cuando JIT ¿Fue presentado? Lo discutiremos en el próximo artículo cuando nos acerquemos a WebAssembly y lo que lo hace potencialmente más rápido que JavaScript.

Campo de golf