javascript - emscripten
¿Por qué asm.js deteriora el rendimiento? (2)
Para ver cómo funciona, escribí un módulo asm.js muy corto a mano, que simula la ecuación de onda 2D utilizando matrices de números enteros de 32 bits y arrays tipeados (Int32Array). Tengo tres versiones, todas tan similares como sea posible:
- JavaScript ordinario (es decir, legible, aunque de estilo C)
- Igual que 1, con anotaciones asm.js agregadas para que pase el validador, de acuerdo con Firefox y otras herramientas
- 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 módulos para ver los efectos de usar cada uno. Los tres funcionan, pero a diferentes velocidades. Este es el punto de acceso (con anotaciones 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 módulos 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 anotaciones requeridas por asm.js empeora el rendimiento 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 rendimiento triple que no se puede superar diciéndole al navegador que use asm.js. Además, ¿por qué decirle al navegador que use asm.js solo ayuda marginalmente en Firefox, y es completamente contraproducente en Chrome?
En realidad, asm.js no se ha creado para escribir código a mano, sino solo como resultado de una compilación 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 función plain js tiene 137 nodos y 746 tokens
- la función 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 contextos 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 memoria utilizando matrices tipadas y evitar muchas recolecciones de basura. Sugeriría reescribir el generador de perfiles y medir asm.js dentro de asm.js, sin cambiar el contexto.