javascript - funciones - jquery
Llamada a función lenta en V8 cuando se usa la misma tecla para las funciones en diferentes objetos (2)
Tal vez no porque la llamada es lenta, sino que la búsqueda es; No estoy seguro, pero aquí hay un ejemplo:
var foo = {};
foo.fn = function() {};
var bar = {};
bar.fn = function() {};
console.time(''t'');
for (var i = 0; i < 100000000; i++) {
foo.fn();
}
console.timeEnd(''t'');
Probado en win8.1
- Firefox 35.01: ~ 240 ms
- Chrome 40.0.2214.93 (V8 3.30.33.15): ~ 760ms
- msie 11: 34 sec
- nodejs 0.10.21 (V8 3.14.5.9): ~ 100ms
- iojs 1.0.4 (V8 4.1.0.12): ~ 760ms
Ahora aquí está la parte interesante, si cambio bar.fn
a bar.somethingelse
:
- Chrome 40.0.2214.93 (V8 3.30.33.15): ~ 100 ms
- nodejs 0.10.21 (V8 3.14.5.9): ~ 100ms
- iojs 1.0.4 (V8 4.1.0.12): ~ 100ms
Algo salió mal en v8 últimamente? ¿Qué causa esto?
Los literales de objeto comparten la clase oculta ("mapa" en términos internos de v8) por la estructura IE mismas claves con nombre en el mismo orden, mientras que los objetos creados desde diferentes constructores tendrían diferente clase oculta incluso si los constructores los inicializaron exactamente en los mismos campos.
Al generar código para foo.fn()
, en el compilador generalmente no se tiene acceso al objeto foo
específico, sino solo a su clase oculta. Desde la clase oculta se puede acceder a la función fn
pero debido a que la clase oculta compartida realmente puede tener diferentes funciones en la propiedad fn
, eso no es posible. Entonces, debido a que no sabe en el momento de la compilación a qué función se llamará, no puede alinear la llamada .
Si ejecuta el código con la marca de trazado en línea:
$ /c/etc/iojs.exe --trace-inlining test.js
t: 651ms
Sin embargo, si cambia algo para que .fn
sea siempre la misma función o foo
y la bar
tenga una clase oculta diferente:
$ /c/etc/iojs.exe --trace-inlining test.js
Inlined foo.fn called from .
t: 88ms
(Hice esto haciendo bar.asd = 3
antes de la bar.fn
bar.fn, pero hay muchas maneras diferentes de lograrlo, como constructores y prototipos que seguro sabrás que son el camino a seguir para javascript de alto rendimiento)
Para ver qué cambió entre versiones, ejecute este código:
var foo = {};
foo.fn = function() {};
var bar = {};
bar.fn = function() {};
foo.fn();
console.log("foo and bare share hidden class: ", %HaveSameMap(foo, bar));
Como puede ver, los resultados difieren entre node10 y iojs:
$ /c/etc/iojs.exe --allow-natives-syntax test.js
foo and bare share hidden class: true
$ node --allow-natives-syntax test.js
foo and bare share hidden class: false
No he seguido el desarrollo de v8 en detalle recientemente, así que no pude señalar la razón exacta, pero estos tipos de heurística cambian todo el tiempo en general.
IE11 es de fuente cerrada, pero de todo lo que han documentado parece que es muy similar a v8.
Primeros fundamentos.
V8 utiliza clases ocultas conectadas con transiciones para descubrir la estructura estática en los esponjosos objetos sin forma de JavaScript.
Las clases ocultas describen la estructura del objeto, las transiciones enlazan las clases ocultas y describen qué clase oculta se debe usar si se realiza una determinada acción en un objeto.
Por ejemplo, el siguiente código daría lugar a la siguiente cadena de clases ocultas:
var o1 = {};
o1.x = 0;
o1.y = 1;
var o2 = {};
o2.x = 0;
o2.y = 0;
Esta cadena se crea cuando construyes o1
. Cuando se construye o2
V8 simplemente sigue las transiciones establecidas.
Ahora, cuando se usa una propiedad fn
para almacenar una función, V8 intenta darle a esta propiedad un tratamiento especial: en lugar de simplemente declarar en la clase oculta que el objeto contiene una propiedad, fn
V8 pone la función en la clase oculta .
var o = {};
o.fn = function fff() { };
Ahora hay una consecuencia interesante aquí: si almacena diferentes funciones en el campo con el mismo nombre, V8 ya no puede seguir simplemente las transiciones porque el valor de la propiedad de la función no coincide con el valor esperado:
var o1 = {};
o1.fn = function fff() { };
var o2 = {};
o2.fn = function ggg() { };
Al evaluar o2.fn = ...
asignación V8 verá que hay una transición etiquetada fn
pero conduce a una clase oculta que no es adecuada: contiene fff
en la propiedad fn
, mientras intentamos almacenar ggg
. Nota: He dado nombres de funciones solo por simplicidad: V8 no usa internamente sus nombres, sino su identidad .
Debido a que V8 no puede seguir esta transición, V8 decidirá que su decisión de promover la función a la clase oculta fue incorrecta y derrochadora. La imagen cambiará
V8 creará una nueva clase oculta donde fn
es simplemente una propiedad simple y no una propiedad de función constante. Redirigirá la transición y también marcará el objetivo de transición anterior obsoleto . Recuerde que o1
todavía lo está usando. Sin embargo, el próximo código de tiempo toca o1
por ejemplo, cuando se carga una propiedad desde él: el tiempo de ejecución migrará o1
a la clase oculta obsoleta. Esto se hace para reducir el polimorfismo: no queremos que o1
y o2
tengan diferentes clases ocultas.
¿Por qué es importante tener funciones en las clases ocultas? Porque esto le da a V8 la optimización de la información del compilador que usa para las llamadas al método en línea . Solo puede llamar al método en línea si el destino de la llamada se almacena en la clase oculta misma.
Ahora apliquemos este conocimiento al ejemplo anterior.
Debido a que hay un choque entre las transiciones bar.fn
y foo.fn
convierten en propiedades normales, con funciones almacenadas directamente en esos objetos y V8 no puede foo.fn
la llamada de foo.fn
lo que lleva a un rendimiento más lento.
¿Podría alinear la llamada antes? Sí . Esto es lo que cambió: en el V8 antiguo no existía ningún mecanismo de desaprobación, por lo que incluso después de que tuvimos un choque y se reencaminó la transición fn
, foo
no se migró a la clase oculta donde fn
convierte en una propiedad normal. En cambio, foo
todavía conservaba la clase oculta donde fn
es una propiedad de función constante directamente integrada en la clase oculta que permite optimizar el compilador para alinearlo.
Si intentas temporizar bar.fn
en el nodo más antiguo, verás que es más lento:
for (var i = 0; i < 100000000; i++) {
bar.fn(); // can''t inline here
}
precisamente porque usa una clase oculta que no permite optimizar el compilador a la llamada bar.fn
línea.
Ahora, lo último que hay que notar aquí es que este punto de referencia no mide el rendimiento de una llamada a función, sino que mide si la optimización del compilador puede reducir este bucle a un bucle vacío al subrayar la llamada dentro de él.