¿Por qué asm.js deteriora el performance?

Para ver cómo funciona, escribí un module asm.js muy corto a mano, que simula la ecuación de onda 2D utilizando matrices de numbers integers de 32 bits y arrays tipeados (Int32Array). Tengo tres versiones, todas tan similares como sea posible:

  1. JavaScript ordinario (es decir, legible, aunque de estilo C)
  2. Igual que 1, con annotations asm.js agregadas para que pase el validador, de acuerdo con Firefox y otras herramientas
  3. Igual que 2, excepto que no tiene "use asm"; directiva en la parte superior

Dejé una demostración en http://jsfiddle.net/jtiscione/xj0x0qk3/ que te permite cambiar entre modules para ver los efectos de usar cada uno. Los tres funcionan, pero a diferentes velocidades. Este es el punto de acceso (con annotations asm.js):

for (i = 0; ~~i < ~~h; i = (1 + i)|0) { for (j = 0; ~~j < ~~w; j = (1 + j)|0) { if (~~i == 0) { index = (1 + index) | 0; continue; } if (~~(i + 1) == ~~h) { index = (1 + index) | 0; continue; } if (~~j == 0) { index = (1 + index) | 0; continue; } if (~~(j + 1) == ~~w) { index = (1 + index) | 0; continue; } uCen = signedHeap [((u0_offset + index) << 2) >> 2] | 0; uNorth = signedHeap[((u0_offset + index - w) << 2) >> 2] | 0; uSouth = signedHeap[((u0_offset + index + w) << 2) >> 2] | 0; uWest = signedHeap [((u0_offset + index - 1) << 2) >> 2] | 0; uEast = signedHeap [((u0_offset + index + 1) << 2) >> 2] | 0; uxx = (((uWest + uEast) >> 1) - uCen) | 0; uyy = (((uNorth + uSouth) >> 1) - uCen) | 0; vel = signedHeap[((vel_offset + index) << 2) >> 2] | 0; vel = vel + (uxx >> 1) | 0; vel = applyCap(vel) | 0; vel = vel + (uyy >> 1) | 0; vel = applyCap(vel) | 0; force = signedHeap[((force_offset + index) << 2) >> 2] | 0; signedHeap[((u1_offset + index) << 2) >> 2] = applyCap(((applyCap((uCen + vel) | 0) | 0) + force) | 0) | 0; force = force - (force >> forceDampingBitShift) | 0; signedHeap[((force_offset + index) << 2) >> 2] = force; vel = vel - (vel >> velocityDampingBitShift) | 0; signedHeap[((vel_offset + index) << 2) >> 2] = vel; index = (index + 1)|0; } } 

La versión de "JavaScript normal" está estructurada como se indicó anteriormente, pero sin los operadores bit a bit que requiere asm.js (por ejemplo, "x | 0", "~~ x", "arr [(x << 2) >> 2]", etc.)

Estos son los resultados de los tres modules en mi máquina, usando Firefox (Developer Edition v. 41) y Chrome (versión 44), en milisegundos por iteración:

  • FIREFOX (versión 41): 20 ms, 35 ms, 60 ms.
  • CROMO (versión 44): 25 ms, 150 ms, 75 ms.

Así que el JavaScript común gana en ambos buscadores. La presencia de annotations requeridas por asm.js empeora el performance en un factor de 3 en ambos. Además, la presencia del "uso asm"; La directiva tiene un efecto obvio: ¡ayuda un poco a Firefox y pone a Chrome de rodillas!

Parece extraño que la mera adición de operadores bit a bit introduzca una degradación de performance triple que no se puede superar diciéndole al browser que use asm.js. Además, ¿por qué decirle al browser que use asm.js solo ayuda marginalmente en Firefox, y es completamente contraproducente en Chrome?

En realidad asm.js no ha sido creado para escribir código a mano, sino solo como resultado de una compilation de otros lenguajes. Hasta donde sé, no hay herramientas que validen el código asm.js. ¿Has intentado escribir el código en Cng y usar Emscripten para generar el código asm.js? Sospecho fuertemente que el resultado sería bastante diferente y optimizado para asm.js.

Creo que mezclando vars tipeados y no tipificados solo agregas complejidad sin ningún beneficio. Por el contrario, el código "asm.js" es más complejo: intenté analizar las funciones asm.js y plain en jointjs.com/demos/javascript-ast y los resultados son:

  • la function plain js tiene 137 nodos y 746 tokens
  • la function asm.js tiene 235 nodos y 1252 tokens

Diría que si tienes más instrucciones para ejecutar en cada ciclo, será más lento.

Existe un costo fijo para cambiar los contexts de asm.js. Lo ideal es hacerlo una vez y ejecutar todo su código dentro de su aplicación como asm.js. Luego puede controlar la administración de memory utilizando matrices tipadas y evitar muchas recollections de basura. Sugeriría reescribir el generador de perfiles y medir asm.js dentro de asm.js, sin cambiar el context.