Cómo puede WebAssembly ejecutar JavaScript rápidamente
(bytecodealliance.org)Intro
-
Cuando se ejecuta JS en el navegador, corre rápido porque el motor de JS del navegador está muy bien optimizado, pero hoy en día también se usa mucho JS en otros entornos. (serverless, consolas de videojuegos, iOS, etc.)
-
WASM es una tecnología que permite ejecutar JS rápidamente en estos entornos de runtime.
Cómo funciona
-
Si hay un motor de JS, el código JS se transforma en bytecode mediante un intérprete y un compilador JIT, entre otros.
-
En entornos sin motor de JS, hay que distribuir el motor junto con el código, y al distribuir el motor de JS como un módulo WASM se vuelve portable entre distintos entornos.
-
El código JS se ejecuta dentro de un motor de JS aislado dentro del motor WASM.
-
El motor de JS que usa el motor WASM es SpiderMonkey, que también usa Firefox.
-
WASM no puede generar código máquina por sí mismo, así que tiene que pasar por compilación con JS.
-
Pero como no puede usar JIT, lo normal sería que WASM fuera lento. Entonces, ¿cómo hace WASM exactamente para ejecutar JS “rápidamente”?
Dónde se usa WASM
Usar JS en iOS (o en entornos donde no se puede usar JIT)
- Consolas de videojuegos, apps iOS sin privilegios, smart TVs, etc., no pueden usar JIT por motivos de seguridad.
(→ Se menciona como si fuera obvio que la compilación JIT tiene problemas de seguridad, pero por más que busqué no me quedó muy claro por qué.)
- Por eso en esos casos hay que usar un intérprete, pero en realidad las apps que corren en estas plataformas suelen ejecutarse durante mucho tiempo y tienen bastante código, así que conviene evitar la lentitud de usar un intérprete.
- Entonces, ¿cómo se puede usar JS evitando la caída de rendimiento del intérprete?
Usar JS en serverless
- En entornos serverless sí existe JIT, pero el problema es que el tiempo de cold start es largo y eso aumenta la latencia. (solo cargar el motor toma al menos 5 ms)
- Hay técnicas de optimización para ocultar el tiempo de cold start, pero a medida que mejora la capa de red (p. ej., QUIC) su utilidad disminuye mucho, y si se ejecutan varias funciones serverless al mismo tiempo estas técnicas también dejan de servir bastante.
- También se puede evitar el cold start reutilizando instancias, pero eso significa que el estado se comparte entre requests y se convierte en un riesgo de seguridad.
- Por estas razones, en la práctica también se está volviendo común meter demasiadas cosas en una sola función serverless en vez de seguir las mejores prácticas.
- Es decir, si se resolviera el problema del cold start, ya no haría falta usar varias de estas técnicas para esquivarlo y se solucionarían muchos problemas.
- WASM encapsula y aísla JS, y como el propio código de WASM es corto y simple, también es más fácil de supervisar y se reducen los riesgos de seguridad.
En qué consume más tiempo un motor de JS
Fase de inicialización
- (inicialización del engine) Corresponde al caso serverless. Tiene que prepararse a sí mismo y agregar funciones built-in al entorno. Esta es una de las razones por las que el cold start en serverless es lento.
- (inicialización de la aplicación) Parsear funciones a bytecode, asignar memoria a variables, asignar valores a variables
Fase de runtime
- A partir de aquí, el throughput depende de varias condiciones.
- qué características del lenguaje se usan
- si el código se comporta de forma predecible desde el punto de vista del motor de JS
- qué tipo de estructuras de datos se usan
- si el código se ejecuta el tiempo suficiente como para beneficiarse del compilador optimizador del motor de JS
Hacer más rápido un motor de JS significa acelerar tanto la fase de inicialización como la fase de runtime. Más exactamente, se trata de reducir el tiempo de inicialización y aumentar el throughput en runtime, es decir, la velocidad de procesamiento del código.
Reducir el tiempo de inicialización
-
WASM reduce el tiempo de inicialización usando un pre-initializer llamado Wizer. (en apps pequeñas, JS on WASM es aproximadamente 13 veces más rápido que un JS isolate)
-
En la etapa de build, antes de distribuir el código, el pre-initializer ejecuta una vez todo el código JS hasta la etapa de inicialización.
-
Al hacer esto, el código JS queda almacenado como bytecode en la memoria lineal del motor de JS, y la asignación de memoria también ya está completada.
-
Luego eso se copia tal cual y se pega en la sección de datos de WASM.
-
-
Cuando se instancia el motor de JS, puede acceder a todos los datos de la sección de datos. Si necesita cierta memoria, puede copiarla desde esa sección. Por eso no hace falta tiempo de arranque, y por eso se le llama pre-inicialización.
-
Actualmente la sección de datos se adjunta al mismo módulo que el motor de JS, pero en el futuro planean usar module linking para convertir la sección de datos en un módulo separado, de modo que varias aplicaciones puedan compartir el motor de JS.
-
Y de hecho esta técnica de pre-inicialización no tiene por qué limitarse al motor de JS; es un concepto que puede usarse en cualquier runtime como Python, Ruby o Lua.
Aumentar el throughput
-
Si el código JS solo se ejecuta durante poco tiempo, de todos modos no pasa por JIT, así que el throughput de WASM será igual al del navegador. Pero en código que se ejecuta durante mucho tiempo, la diferencia de throughput causada por la presencia o ausencia de JIT es grande.
-
Como WASM no puede usar JIT, en su lugar adopta un enfoque de compilación AOT (ahead-of-time), tomando al mismo tiempo las técnicas que sí pueden aprovecharse del JIT.
-
Una de las técnicas de optimización de JIT es el inline caching: conservar fragmentos de código ejecutados antes para reutilizarlos.
-
En WASM se preparan como stubs los patrones que se usan con frecuencia en JS. Por ejemplo, acceder a propiedades de objetos.
-
Normalmente, para hacer bien un acceso a propiedades de objetos se necesita información de shape y offset, y eso no puede saberse con AOT.
-
Pero sí se puede preparar de antemano un stub que acceda a la propiedad usando shape y offset como parámetros. Ese código stub puede reutilizarse en muchos lugares.
-
-
WASM convierte todos estos common patterns en stubs. Esto no depende de cómo esté escrito realmente el código JS. Con esto se reduce la cantidad de código máquina que el motor de JS tiene que generar, también se reduce el tiempo de inicialización y mejora la localidad de caché.
-
Se comprobó que con solo preparar 2kb de estos stubs se puede cubrir aproximadamente el 95% del código JS real.
-
Como esta técnica optimiza ahead-of-time, es decir, sin conocer el contenido del código (sin profiling), probablemente haya margen para optimizar aún más, como hace JIT, si se hace más profiling.
- Pero como el profiling en sí no es sencillo, todavía están trabajando en ello.
2 comentarios
Sobre los problemas de seguridad del JIT, antes se mencionó algo relacionado en una entrada del blog del equipo de MS Edge que fue presentada aquí. Básicamente, como los motores JIT son complejos, no solo aumenta la superficie de ataque, sino que además métodos como la optimización especulativa (Speculative Optimization), que el JIT aplica para mejorar el rendimiento, aparentemente tienden a provocar de forma repetida ciertos patrones de problemas de seguridad. Por eso, se dice que la proporción de fallas de seguridad relacionadas con JIT entre las vulnerabilidades de seguridad de los navegadores web es bastante alta.
https://es.news.hada.io/topic?id=4771
https://microsoftedge.github.io/edgevr/posts/Super-Duper-Secure-Mode/
https://docs.google.com/spreadsheets/d/…
¡Oh, gracias! Justo no había buscado en GeekNews.