Optimización del rendimiento: JavaScript (V8) vs AssemblyScript (WebAssembly)





Para mejorar el rendimiento de las aplicaciones web, utilice WebAssembly junto con AssemblyScript para reescribir los componentes JavaScript críticos para el rendimiento de una aplicación web. "¿Y realmente ayudará?", Preguntas.



Desafortunadamente, no hay una respuesta clara a esta pregunta. Todo depende de cómo los uses. Hay muchas opciones: en algunos casos la respuesta será negativa, en otros será positiva. En una situación, es mejor elegir JavaScript en lugar de AssemblyScript, y en otra, es al revés. Esto está influenciado por muchas condiciones diferentes.



En este artículo, analizaremos estas condiciones, propondremos una serie de soluciones y las probaremos en varios ejemplos de código de prueba.



¿Quién soy y por qué estoy haciendo este tema? 



(Puede omitir esta sección, no es esencial para comprender más material).



Realmente me gusta el lenguaje AssemblyScript . Incluso comencé a ayudar financieramente a los desarrolladores en algún momento. Tienen un pequeño equipo en el que todos están muy apasionados por este proyecto. AssemblyScript es un lenguaje muy joven similar a TypeScript capaz de compilar en WebAssembly (Wasm). Ésta es precisamente una de sus ventajas. Anteriormente, para usar Wasm, un desarrollador web tenía que aprender idiomas extranjeros como C, C ++, C #, Go o Rust, ya que estos lenguajes de alto nivel con tipado estático se podían compilar en WebAssembly desde el desde el comienzo. 



Aunque AssemblyScript (ASC) es similar a TypeScript (TS), no está asociado con ese lenguaje y no se compila en JS. Se necesita similitud en la sintaxis y la semántica para facilitar el proceso de "migración" de TS a ASC. Esta portabilidad básicamente se reduce a agregar anotaciones de tipo.



Siempre estuve interesado en tomar código JS, portarlo a ASC, compilarlo a Wasm y comparar el rendimiento. Cuando mi colega Ingvar me envió un fragmento de JavaScript para difuminar las imágenes , decidí usarlo. Hice un pequeño experimento para ver si valía la pena explorar este tema más profundamente. Resultó que valió la pena. Como resultado, apareció este artículo.



Para conocer mejor AssemblyScript, puede visitar el sitio web oficial , unirse al canal de Discord o ver el video introductorio en mi canal de Youtube. Y seguimos adelante.



Beneficios de WebAssembly



Como escribí anteriormente, durante mucho tiempo la tarea principal de Wasm fue la capacidad de compilar código escrito en lenguajes de propósito general de alto nivel. Por ejemplo, en Squoosh (una herramienta de procesamiento de imágenes en línea) utilizamos bibliotecas del ecosistema C / C ++ y Rust. Estas bibliotecas no se diseñaron originalmente para su uso en aplicaciones web, pero WebAssembly lo hace posible.



Además, según la creencia popular, la compilación del código fuente en Wasm también es necesaria porque el uso de binarios de Wasm le permite acelerar el trabajo de una aplicación web. Estoy de acuerdo, como mínimo, en que, en condiciones ideales (de laboratorio), los binarios de WebAssembly y JavaScript puedenproporcionan valores aproximadamente iguales de rendimiento máximo. Esto es casi imposible en proyectos web de combate. 



En mi opinión, tiene más sentido pensar en WebAssembly como una herramienta de optimización para valores de rendimiento de trabajo promedio. Aunque recientemente, Wasm tiene la capacidad de usar instrucciones SIMD y flujos de memoria compartida. Esto debería aumentar su competitividad. Pero en cualquier caso, como escribí anteriormente, todo depende de la situación específica y las condiciones iniciales.



A continuación, consideraremos varias de estas condiciones:



Falta de calentamiento



El motor V8 JS procesa el código fuente y lo presenta como un árbol de sintaxis abstracta (AST). Basado en el AST construido, el intérprete de Ignition optimizado genera un código de bytes. El compilador Sparkplug toma el código de bytes resultante y, en la salida, produce el código de máquina aún no optimizado, con una gran huella. Durante la ejecución del código, V8 recopila información sobre los formularios (tipos) de los objetos utilizados y luego ejecuta el compilador de optimización TurboFan. Genera instrucciones de máquina de bajo nivel optimizadas para la arquitectura de destino en función de la información recopilada sobre los objetos.



Puede comprender cómo funcionan los motores JS al estudiar la traducción de este artículo .





Canalización del motor JS. Esquema general



Por otro lado, WebAssembly utiliza escritura estática, por lo que puede generar inmediatamente código máquina a partir de él. El motor V8 tiene un compilador Wasm de transmisión llamado Liftoff. Al igual que Ignition, le ayuda a preparar y ejecutar rápidamente código no optimizado. Y después de eso, el mismo TurboFan se activa y optimiza el código de la máquina. Se ejecutará más rápido que después de compilar Liftoff, pero tardará más en generarse. 



La diferencia fundamental entre la canalización de JavaScript y la canalización de WebAssembly: el motor V8 no necesita recopilar información sobre objetos y tipos, ya que Wasm tiene tipado estático y todo se conoce de antemano. Esto ahorra tiempo.



Falta de optimización



El código de máquina que TurboFan genera para JavaScript solo se puede utilizar siempre que se mantengan los supuestos de tipo. Digamos que TurboFan ha generado código de máquina, por ejemplo, para una función f con un parámetro numérico. Luego, al encontrar una llamada a esta función con un objeto en lugar de un número, el motor vuelve a utilizar Ignition o Sparkplug. Esto se llama desoptimización.



Para WebAssembly, los tipos no pueden cambiar durante la ejecución del programa. Por lo tanto, no hay necesidad de dicha desoptimización. Y los tipos en sí que admite Wasm se traducen orgánicamente a código de máquina.



Minimizar binarios para grandes proyectos



Wasm fue diseñado originalmente con el formato de archivo binario compacto en mente. Por lo tanto, estos binarios se cargan rápidamente. Pero en muchos casos, todavía resultan más de lo que nos gustaría (al menos en términos de los volúmenes aceptados en la red). Sin embargo, con gzip o brotli, estos archivos se comprimirán bien.



A lo largo de los años, JavaScript ha aprendido muchas cosas de forma inmediata: matrices, objetos, diccionarios, iteradores, procesamiento de cadenas, herencia de prototipos, etc. Todo esto está integrado en su motor. Y el lenguaje C ++, por ejemplo, puede presumir de un alcance mucho mayor. Y cada vez que use cualquiera de estas abstracciones de lenguaje al compilar en WebAssembly, el código correspondiente debajo del capó debe incluirse en su binario. Esta es una de las razones de la proliferación de binarios de WebAssembly. 



Wasm realmente no sabe nada sobre C ++ (o cualquier otro idioma). Por lo tanto, el tiempo de ejecución de Wasm no proporciona una biblioteca C ++ estándar y el compilador tiene que agregarla a cada archivo binario. Pero dicho código debe conectarse solo una vez. Por lo tanto, para proyectos más grandes, esto no afecta en gran medida el tamaño resultante del binario Wasm, que al final suele ser más pequeño que otros binarios.



Está claro que no en todos los casos es posible tomar una decisión informada comparando solo los tamaños de los binarios. Si, por ejemplo, el código fuente de AssemblyScript se compila en Wasm, entonces el binario resultará realmente muy compacto. Pero, ¿qué tan rápido se ejecutará? Me propuse la tarea de comparar diferentes versiones de binarios JS y ASC en función de dos criterios a la vez: velocidad y tamaño. 



Portar a AssemblyScript



Como ya escribí, TypeScript y ASC son muy similares en sintaxis y semántica. Es fácil suponer que hay similitudes con JS, por lo que la migración se trata principalmente de agregar anotaciones de tipo (o reemplazar tipos). Para comenzar a portar Glur , JS-library para desenfoque de imagen.



Mapeo de tipos de datos



Los tipos de AssemblyScript integrados se implementan de manera similar a los tipos de máquina virtual Wasm (WebAssembly VM). Si en TypeScript, por ejemplo, el tipo de número se implementa como un número de punto flotante de 64 bits (según el estándar IEEE754), entonces en ASC hay varios tipos numéricos: u8, u16, u32, i8, i16, i32 , f32 y f64. Además, puede encontrar tipos de datos compuestos comunes (cadena, Array, ArrayBuffer, Uint8Array, etc.) en la biblioteca estándar de AssemblyScript, que, con ciertas reservas, están presentes en TypeScript y JavaScript. No consideraré aquí las tablas de mapeo de tipos AssemblyScript, TypeScript y Wasm VM, este es un tema para otro artículo. Lo único que quiero señalar es que ASC implementa el tipo StaticArray, para el cual no he encontrado análogos en JS y WebAssembly VM.



Finalmente, pasamos a nuestro código de ejemplo de la biblioteca glur. 



JavaScript:



function gaussCoef(sigma) {

  if (sigma < 0.5)

    sigma = 0.5;

  var a = Math.exp(0.726 * 0.726) / sigma;

  /* ... more math ... */

  return new Float32Array([

    a0, a1, a2, a3, 

    b1, b2, 

    left_corner, right_corner

  ]);

}

AssemblyScript:

function gaussCoef(sigma: f32): Float32Array {

  if (sigma < 0.5

    sigma = 0.5;

  let a: f32 = Mathf.exp(0.726 * 0.726) / sigma;

  /* ... more math ... */

  const r = new Float32Array(8);

  const v = [

    a0, a1, a2, a3, 

    b1, b2, 

    left_corner, right_corner

  ];

  for (let i = 0; i < v.length; i++) {

    r[i] = v[i];

  }

  return r;

}
      
      







El fragmento de código de AssemblyScript contiene un bucle adicional al final, ya que no hay forma de inicializar la matriz a través del constructor. ASC no implementa la sobrecarga de funciones, por lo que en este caso solo tenemos un constructor Float32Array (lengthOfArray: i32). AssemblyScript tiene devoluciones de llamada, pero no cierres, por lo que no hay forma de usar .forEach () para completar una matriz con valores. Así que tuve que usar un bucle for regular para copiar un elemento a la vez.



 Es posible que haya notado que en el fragmento de código de AssemblyScript Math, Mathf. , 64- , — 32-. Math . - , , f32. . .



:



Me tomó mucho tiempo entenderlo: la elección de los tipos es muy importante. Desenfocar la imagen implica operaciones de convolución, y eso es un montón de bucles for que atraviesan todos los píxeles. Fue ingenuo pensar que si todos los índices de píxeles son positivos, los contadores de bucle también serán positivos. No debería haber elegido el tipo u32 (entero sin signo de 32 bits) para ellos. Si alguno de estos bucles se ejecuta en la dirección opuesta, se volverá infinito (el programa hará un bucle):



let j: u32; 

// ... many many lines of code ...

for (j = width — 1; j >= 0; j--) {

  // ...

}
      
      







No encontré ninguna otra dificultad en la portabilidad.



Puntos de referencia de shell D8



De acuerdo, los fragmentos de código bilingües están listos. Ahora puede compilar ASC en Wasm y ejecutar los primeros puntos de referencia.



Unas pocas palabras sobre d8: este es un shell de comandos para el motor V8 (él mismo no tiene su propia interfaz), que le permite realizar todas las acciones necesarias tanto con Wasm como con JS. En principio, d8 se puede comparar con Node, que de repente cortó la biblioteca estándar y solo quedó ECMAScript puro. Si no tiene una versión compilada de V8 en la configuración regional ( aquí se describe cómo compilarla ), no puede usar d8. Para instalar d8 use la herramienta jsvu . 



Sin embargo, dado que el título de esta sección contiene la palabra "Puntos de referencia", me parece importante proporcionar algún tipo de descargo de responsabilidad aquí: los números y los resultados que recibí se refieren al código que escribí en los idiomas de mi elección que se ejecutan en mi computadora (MacBook Air M1 2020) usando los scripts de prueba que creé. En el mejor de los casos, los resultados son pautas aproximadas. Por lo tanto, sería imprudente dar estimaciones cuantitativas generalizadas del rendimiento de AssemblyScript con WebAssembly o JavaScript con V8 basado en ellos.



Es posible que tenga otra pregunta: ¿por qué elegí d8 y no ejecuté scripts en el navegador o en el nodo? Creo que tanto el navegador como Node, digamos, no son lo suficientemente estériles para mis experimentos. Además de la esterilidad necesaria, d8 permite controlar la tubería del motor V8. Puedo capturar cualquier escenario de optimización y usar, por ejemplo, solo Ignition, solo Sparkplug o Liftoff para que las características de rendimiento no cambien en medio de la prueba.



Técnica experimental



Como escribí anteriormente, tenemos la oportunidad de "calentar" el motor JavaScript antes de ejecutar la prueba de rendimiento. Durante este proceso de calentamiento, el V8 realiza la optimización necesaria. Así que ejecuté el programa de desenfoque 5 veces antes de comenzar las mediciones, luego ejecuté 50 carreras e ignoré las 5 carreras más rápidas y lentas para eliminar posibles valores atípicos y demasiados valores atípicos. 



Mira lo que pasó:







Por un lado, me alegré de que Liftoff produjera un código más rápido en comparación con Ignition y Sparkplug. Pero el hecho de que AssemblyScript, compilado en Wasm usando optimización, resultó ser varias veces más lento que el paquete de JavaScript - TurboFan, estaba desconcertado.



Aunque más tarde admití que las fuerzas no eran inicialmente iguales: un gran equipo de ingenieros ha estado trabajando en JS y su motor V8 durante muchos años, implementando la optimización y otras cosas inteligentes. AssemblyScript es un proyecto relativamente joven con un equipo pequeño. El compilador ASC en sí es de un solo paso y pone todos los esfuerzos de optimización en la biblioteca Binaryen... Esto significa que la optimización se realiza en el nivel de código de bytes de Wasm VM después de que ya se haya compilado la mayor parte de la semántica de alto nivel. El V8 tiene una clara ventaja aquí. Sin embargo, el código de desenfoque es muy simple: son las operaciones aritméticas habituales con valores de la memoria. Parecía que ASC y Wasm deberían haberlo hecho mejor con esta tarea. ¿Qué ocurre aquí?



Profundicemos



Consulté rápidamente a los chicos inteligentes del equipo V8 y a los chicos igualmente inteligentes del equipo de AssemblyScript (¡gracias a Daniel y Max!). Resultó que al compilar ASC, la "verificación de límites" (valores límite) no se inicia.



V8 puede mirar el código fuente JS en cualquier momento y comprender su semántica. Utiliza esta información para una optimización adicional o repetida. Por ejemplo, tiene un ArrayBuffer que contiene un conjunto de datos binarios. En este caso, V8 espera que sea más razonable no solo ejecutar caóticamente a través de celdas de memoria, sino usar un iterador a través de un bucle for ... of. 



for (<i>variable</i> of <i>iterableObject</i>) {

  <i>statement</i>

}

      
      





La semántica de este operador asegura que nunca sobrepasemos los límites de la matriz. En consecuencia, el compilador TurboFan no maneja la verificación de límites. Pero antes de compilar ASC en Wasm, la semántica de AssemblyScript no se utiliza para dicha optimización: toda la optimización se realiza en el nivel de la máquina virtual de WebAssembly.



Afortunadamente, ASC todavía tiene una carta de triunfo bajo la manga: la anotación sin marcar (). Indica qué valores deben comprobarse por la posibilidad de salirse de los límites. 



- prev_prev_out_r = prev_src_r * coeff [6];



- línea [line_index] = prev_out_r;



Las 2 líneas anteriores deben reescribirse de la siguiente manera:



+ prev_prev_out_r = prev_src_r * unchecked (coeff [6]);



+ sin marcar (línea [line_index] = prev_out_r);



Sí, hay más. En AssemblyScript, las matrices escritas (Uint8Array, Float32Array, etc.) se implementan en la imagen y semejanza de ArrayBuffer. Sin embargo, debido a la falta de optimización de alto nivel para acceder al elemento de la matriz con el índice i, cada vez que tiene que acceder a la memoria dos veces: la primera vez para cargar el puntero al primer elemento de la matriz y la segunda vez para cargar el elemento en desplazamiento i * sizeOfType. Es decir, debe referirse a la matriz como un búfer (mediante un puntero). En el caso de JS, la mayoría de las veces esto no sucede, ya que V8 logra hacer una optimización de alto nivel del acceso a la matriz utilizando un único acceso a la memoria.



Para mejorar el rendimiento, AssemblyScript implementa matrices estáticas (StaticArray). Son similares a Array excepto que tienen una longitud fija. Y, en consecuencia, no es necesario almacenar un puntero al primer elemento de la matriz. Si es posible, use estas matrices para acelerar sus programas.



Entonces, tomé el grupo AssemblyScript - TurboFan (funcionó más rápido) y lo llamé ingenuo. Luego agregué las dos optimizaciones de las que hablé en esta sección y obtuve una variante llamada optimizada.







¡Mucho mejor! Hemos logrado un progreso significativo. Sin embargo, AssemblyScript sigue siendo más lento que JavaScript. ¿Es esto todo lo que podemos hacer? [spoiler: no]



Oh, estos silencios



Los chicos del equipo de AssemblyScript también me dijeron que la marca --optimize es equivalente a -O3s. Optimiza bien la velocidad de trabajo, pero no la lleva al máximo, ya que al mismo tiempo evita el crecimiento del archivo binario. La bandera -O3 optimiza solo la velocidad y lo hace hasta el final. Usar -O3s por defecto parece correcto, ya que es habitual en el desarrollo web reducir el tamaño de los binarios, pero ¿merece la pena? Al menos en este ejemplo en particular, la respuesta es no: -O3s ahorra unos miserables ~ 30 bytes, pero pasa por alto una caída significativa en el rendimiento:







Una sola bandera de optimizador simplemente cambia el juego: finalmente, AssemblyScript ha superado a JavaScript (¡en este caso de prueba en particular!). 



Ya no indicaré la bandera O3 en la tabla, pero ten la seguridad: desde ahora hasta el final del artículo, será invisible para nosotros.



Ordenamiento de burbuja



Para asegurarme de que el ejemplo borroso no sea solo un accidente, decidí tomar otro. Tomé la implementación de clasificación en StackOverflow y pasé por el mismo proceso: 



  • portó el código agregando tipos; 
  • lanzó la prueba;
  • optimizado; 
  • volvió a ejecutar la prueba. 


(No he probado la creación y el llenado de una matriz para la clasificación de burbujas).







¡Lo hicimos de nuevo! Esta vez con una ganancia de velocidad aún mayor: el AssemblyScript optimizado es casi el doble de rápido que JavaScript. Pero eso no es todo. Más altibajos me esperan de nuevo. ¡Por favor, no cambies!



Gestión de la memoria



Algunos de ustedes pueden haber notado que estos dos ejemplos realmente no muestran cómo trabajar con la memoria. En JavaScript, V8 se encarga de toda la gestión de la memoria (y la recolección de basura) por usted. En WebAssembly, por otro lado, terminas con un trozo de memoria lineal y tienes que decidir cómo usarlo (o más bien Wasm tiene que decidir). ¿Cuánto cambiará nuestra tabla si usamos el montón de forma intensiva?



Decidí tomar un nuevo ejemplo con una implementación de montón binario... Durante las pruebas, lleno un montón con 1 millón de números aleatorios (cortesía de Math.random ()) y pop () los devuelve, verificando si los números están en orden ascendente. El esquema general de trabajo sigue siendo el mismo: transfiera el código JS a ASC, compile con la configuración ingenua, ejecute las pruebas, optimice y vuelva a ejecutar las pruebas:







¿80 veces más lento que JavaScript con TurboFan? ¡Y 6 veces más lento que Ignition! ¿Qué salió mal?



Configuración del entorno de ejecución



Todos los datos que generamos en AssemblyScript deben almacenarse en la memoria. Pero debemos asegurarnos de no sobrescribir nada más que ya esté allí. Dado que AssemblyScript tiende a imitar el comportamiento de JavaScript, también tiene un recolector de basura y, cuando se compila, agrega este recolector al módulo WebAssembly. ASC no quiere que se preocupe por cuándo asignar y cuándo liberar memoria.



En este modo (llamado incremental) funciona por defecto. Al mismo tiempo, solo se agregan alrededor de 2 KB en el archivo gzip al módulo Wasm. AssemblyScript también ofrece modos alternativos, mínimo y stub. Los modos se pueden seleccionar usando el indicador --runtime. Minimal usa el mismo asignador de memoria, pero un recolector de basura más liviano que no se inicia automáticamente, sino que debe llamarse manualmente. Esto es útil cuando se desarrollan aplicaciones de alto rendimiento (como juegos) en las que desea controlar cuándo el recolector de basura detiene su programa. En el modo stub, se agrega muy poco código al módulo Wasm (~ 400B en formato gzip). Funciona rápidamente, ya que se utiliza un asignador de respaldo ( aquí se escriben más detalles sobre los asignadores ).



Los asignadores redundantes son muy rápidos, pero no pueden liberar memoria. Puede sonar tonto, pero puede ser útil para instancias "únicas" de módulos, donde después de completar la tarea, en lugar de liberar memoria, borra toda la instancia de WebAssembly y crea una nueva.



Veamos cómo funcionará nuestro módulo en diferentes modos:







Tanto el mínimo como el stub nos acercaron mucho más al nivel de rendimiento de JavaScript. ¿Me pregunto porque? Como se mencionó anteriormente, mínimo e incremental usan el mismo asignador. Ambos también tienen un recolector de basura, pero mínimo no lo ejecuta a menos que se llame explícitamente (que yo no). Esto significa que el punto es que incremental inicia la recolección de basura automáticamente y, a menudo, lo hace innecesariamente. Bueno, ¿por qué es esto necesario si solo necesita rastrear una matriz?



Problema de asignación de memoria



Después de ejecutar el módulo Wasm en modo de depuración (--debug) varias veces, descubrí que la velocidad del trabajo se ralentiza debido a la biblioteca libsystem_platform.dylib. Contiene primitivas de nivel de sistema operativo para subprocesos y administración de memoria. Las llamadas a esta biblioteca se realizan desde __new () y __renew (), que a su vez se llaman desde Array # push: Ya veo: hay un problema de administración de memoria aquí. Pero JavaScript de alguna manera se las arregla para procesar rápidamente una matriz en constante crecimiento. Entonces, ¿por qué no puede hacer esto AssemblyScript? Afortunadamente, la fuente de la biblioteca estándar de AssemblyScript está disponible públicamente , así que echemos un vistazo a esta siniestra función push () de la clase Array:



[Bottom up (heavy) profile]:



  ticks parent  name



  18670   96.1%  /usr/lib/system/libsystem_platform.dylib



  13530   72.5%    Function: *~lib/rt/itcms/__renew



  13530  100.0%      Function: *~lib/array/ensureSize



  13530  100.0%        Function: *~lib/array/Array#push



  13530  100.0%          Function: *binaryheap_optimized/BinaryHeap#push



  13530  100.0%            Function: *binaryheap_optimized/push



   5119   27.4%    Function: *~lib/rt/itcms/__new



   5119  100.0%      Function: *~lib/rt/itcms/__renew



   5119  100.0%        Function: *~lib/array/ensureSize



   5119  100.0%          Function: *~lib/array/Array#push



   5119  100.0%            Function: *binaryheap_optimized/BinaryHeap#push












export class Array<T> {

  // ...

  push(value: T): i32 {

    var length = this.length_;

    var newLength = length + 1;

    ensureSize(changetype<usize>(this), newLength, alignof<T>());

    // ...

    return newLength;

  }

  // ...

}
      
      







Hasta ahora, todo es correcto: la nueva longitud de la matriz es igual a la longitud actual, aumentada en 1. A continuación, se realiza una llamada a la función asegurarSize () para asegurarse de que hay suficiente capacidad en el búfer (Capacidad) para el nuevo elemento.



function ensureSize(array: usize, minSize: usize, alignLog2: u32): void {

  // ...

  if (minSize > <usize>oldCapacity >>> alignLog2) {

    // ...

    let newCapacity = minSize << alignLog2;

    let newData = __renew(oldData, newCapacity);

    // ... 

  }

}
      
      







La función asegurarSize (), a su vez, comprueba: ¿La capacidad es menor que el nuevo minSize? Si es así, asigne un nuevo búfer minSize usando la función _renew (). Esto implica copiar todos los datos del búfer antiguo al búfer nuevo. Por esta razón, nuestra prueba de llenar la matriz con 1 millón de valores (un elemento tras otro) conduce a la reasignación de una gran cantidad de memoria y crea mucha basura.



En otras bibliotecas (como std :: vec en Rust o rodajasen Go), el nuevo búfer tiene el doble de capacidad que el anterior, lo que ayuda a que el proceso de trabajar con memoria sea menos costoso y lento. Estoy trabajando en este problema en ASC, y hasta ahora la única solución es crear mi propio CustomArray, con su propia optimización de memoria.







Ahora incremental es tan rápido como mínimo y stub. Pero JavaScript sigue siendo el líder en este caso de prueba. Probablemente podría hacer más optimizaciones a nivel de lenguaje, pero este no es un artículo sobre cómo optimizar AssemblyScript en sí. Ya me he hundido lo suficiente.



Hay muchas optimizaciones simples que el compilador de AssemblyScript podría hacer por mí. Con este fin, el equipo de ASC está trabajando en un optimizador de IR (Representación intermedia) de alto nivel llamado AIR. ¿Puede esto hacer que el trabajo sea más rápido y evitar que tenga que optimizar manualmente el acceso a la matriz cada vez? Más probable. ¿Será más rápido que JavaScript? Difícil de decir. Pero en cualquier caso, fue interesante para mí competir por ASC, evaluar las capacidades de JS y ver qué puede lograr un lenguaje más "maduro" con herramientas de compilación "muy inteligentes".



Óxido y C ++



Reescribí el código en Rust, tan idiomáticamente como pude, y lo compilé en WebAssembly. Resultó ser más rápido que AssemblyScript (ingenuo), pero más lento que nuestro AssemblyScript optimizado con CustomArray. A continuación, optimicé el módulo compilado de Rust de la misma manera que AssemblyScript. Con esta optimización, el módulo Wasm basado en Rust es más rápido que nuestro AssemblyScript optimizado, pero aún más lento que JavaScript.



Tomé el mismo enfoque para C ++, usando Emscripten para compilar en WebAssembly . Para mi sorpresa, incluso la primera opción sin optimización resultó no ser peor que JavaScript.





Aquí no hay URL de imagen. Yo mismo hice una captura de pantalla. De



todos modos, las versiones marcadas como idiomáticas estaban influenciadas por el código fuente de JS. Traté de usar mi conocimiento de los modismos de Rust, C ++, pero la instalación estaba firmemente en mi cabeza que estaba haciendo portar. Estoy seguro de que alguien con más experiencia en estos lenguajes podría implementar la tarea desde cero y el código se vería diferente.



Estoy bastante seguro de que los módulos Rust y C ++ podrían ejecutarse aún más rápido. Pero no tenía un conocimiento profundo suficiente de estos idiomas para sacarles más provecho.



De nuevo sobre el tamaño de los binarios.



Echemos un vistazo a los tamaños de los binarios después de la compresión gzip. En comparación con Rust y C ++, los binarios de AssemblyScript son mucho más ligeros.  







Y sin embargo ... recomendaciones



Escribí sobre esto al principio del artículo y lo repetiré ahora: los resultados son, en el mejor de los casos, pautas aproximadas. Por lo tanto, sería imprudente dar estimaciones cuantitativas generalizadas de la productividad sobre su base. Por ejemplo, no se puede decir que Rust es 1,2 veces más lento que JavaScript en todos los casos. Estos números dependen mucho del código que escribí, las optimizaciones que apliqué y la máquina que utilicé. 



Aún así, creo que hay algunas pautas generales que podemos aprender para ayudarlo a comprender mejor el tema y tomar mejores decisiones:



  • Liftoff AssemblyScript Wasm-, , , Ignition SparkPlug JavaScript. JS-, WebAssembly — .

  • V8 JavaScript-. WebAssembly , JavaScript, , .

  • , , .

  • Los módulos de AssemblyScript son generalmente mucho más ligeros que los módulos Wasm compilados de otros lenguajes. En este artículo, el binario de AssemblyScript no era más pequeño que el binario de JavaScript, pero lo contrario es cierto para los módulos más grandes, según lo declarado por el equipo de desarrollo de ASC.


Si no me cree (y no tiene por qué hacerlo) y quiere averiguar el código de los casos de prueba por su cuenta, aquí está .






Nuestros servidores se pueden utilizar para el desarrollo de WebAssembly.



Regístrese usando el enlace de arriba o haciendo clic en el banner y obtenga un 10% de descuento durante el primer mes de alquiler de un servidor de cualquier configuración.






All Articles