javascript - ¿Cómo funciona Bluebird''s util.toFastProperties que hace que las propiedades de un objeto sean "rápidas"?
node.js performance (1)
En el archivo util.js
de Bluebird, tiene la siguiente función:
function toFastProperties(obj) {
/*jshint -W027*/
function f() {}
f.prototype = obj;
ASSERT("%HasFastProperties", true, obj);
return f;
eval(obj);
}
Por alguna razón, hay una declaración después de la función de retorno, que no estoy seguro de por qué está allí.
Además, parece que es deliberado, ya que el autor había silenciado la advertencia JSHint sobre esto:
"Evaluador" inalcanzable después de "regresar". (W027)
¿Qué hace exactamente esta función? ¿ util.toFastProperties
realmente hace que las propiedades de un objeto sean "más rápidas"?
He buscado en el repositorio de GitHub de Bluebird cualquier comentario en el código fuente o una explicación en su lista de problemas, pero no pude encontrar ninguno.
Actualización de 2017: Primero, para los lectores que lleguen hoy: aquí hay una versión que funciona con Node 7 (4+):
function enforceFastProperties(o) {
function Sub() {}
Sub.prototype = o;
var receiver = new Sub(); // create an instance
function ic() { return typeof receiver.foo; } // perform access
ic();
ic();
return o;
eval("o" + o); // ensure no dead code elimination
}
Sin una o dos pequeñas optimizaciones, todo lo que sigue sigue siendo válido.
Primero analicemos qué hace y por qué es más rápido y luego por qué funciona.
Que hace
El motor V8 usa dos representaciones de objetos:
- Modo diccionario - en qué objeto se almacenan como mapas de valores clave como un mapa hash .
- Modo rápido : en el que los objetos se almacenan como structs , en las que no hay cálculos relacionados con el acceso a la propiedad.
Aquí hay una demostración simple que demuestra la diferencia de velocidad. Aquí usamos la instrucción delete
para forzar los objetos al modo de diccionario lento.
El motor intenta usar el modo rápido siempre que sea posible y, en general, siempre que se realiza un gran número de acceso a la propiedad, sin embargo, a veces se lanza al modo de diccionario. Estar en el modo diccionario tiene una gran penalización de rendimiento, por lo que generalmente es conveniente colocar los objetos en modo rápido.
Este truco está destinado a forzar al objeto a modo rápido desde el modo diccionario.
- Bluebird Petka mismo habla de ello aquí .
- Estas diapositivas de Vyacheslav Egorov también lo mencionan.
- Esta pregunta y su respuesta aceptada también están relacionadas.
- Este artículo ligeramente obsoleto sigue siendo una lectura bastante buena que puede darle una buena idea sobre cómo se almacenan los objetos en v8.
Por qué es más rápido
En JavaScript, los prototipos suelen almacenar funciones compartidas entre muchas instancias y rara vez cambian mucho dinámicamente. Por esta razón, es muy conveniente tenerlos en modo rápido para evitar la penalización adicional cada vez que se llame a una función.
Para esto, v8 con mucho gusto pondrá los objetos que son la propiedad .prototype
de las funciones en modo rápido ya que serán compartidos por cada objeto creado al invocar esa función como un constructor. Esto es generalmente una optimización inteligente y deseable.
Cómo funciona
Veamos primero el código y calculamos qué hace cada línea:
function toFastProperties(obj) {
/*jshint -W027*/ // suppress the "unreachable code" error
function f() {} // declare a new function
f.prototype = obj; // assign obj as its prototype to trigger the optimization
// assert the optimization passes to prevent the code from breaking in the
// future in case this optimization breaks:
ASSERT("%HasFastProperties", true, obj); // requires the "native syntax" flag
return f; // return it
eval(obj); // prevent the function from being optimized through dead code
// elimination or further optimizations. This code is never
// reached but even using eval in unreachable code causes v8
// to not optimize functions.
}
No tenemos que encontrar el código nosotros mismos para afirmar que v8 hace esta optimización, en su lugar podemos leer las pruebas de la unidad v8 :
// Adding this many properties makes it slow.
assertFalse(%HasFastProperties(proto));
DoProtoMagic(proto, set__proto__);
// Making it a prototype makes it fast again.
assertTrue(%HasFastProperties(proto));
La lectura y ejecución de esta prueba nos muestra que esta optimización sí funciona en v8. Sin embargo, sería bueno ver cómo.
Si comprobamos objects.cc
podemos encontrar la siguiente función (L9925):
void JSObject::OptimizeAsPrototype(Handle<JSObject> object) {
if (object->IsGlobalObject()) return;
// Make sure prototypes are fast objects and their maps have the bit set
// so they remain fast.
if (!object->HasFastProperties()) {
MigrateSlowToFast(object, 0);
}
}
Ahora, JSObject::MigrateSlowToFast
toma explícitamente el Diccionario y lo convierte en un objeto V8 rápido. Es una lectura que vale la pena y una visión interesante de las partes internas de objetos v8, pero aquí no es el tema. Todavía te recomiendo encarecidamente que lo leas aquí ya que es una buena forma de aprender sobre los objetos v8.
Si miramos SetPrototype
en SetPrototype
, podemos ver que se llama en la línea 12231:
if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Que a su vez es llamado por FuntionSetPrototype
que es lo que obtenemos con .prototype =
.
Hacer __proto__ =
o .setPrototypeOf
también habría funcionado, pero estas son funciones de ES6 y Bluebird se ejecuta en todos los navegadores desde Netscape 7, por lo que está fuera de cuestión simplificar el código aquí. Por ejemplo, si revisamos .setPrototypeOf
, podemos ver:
// ES6 section 19.1.2.19.
function ObjectSetPrototypeOf(obj, proto) {
CHECK_OBJECT_COERCIBLE(obj, "Object.setPrototypeOf");
if (proto !== null && !IS_SPEC_OBJECT(proto)) {
throw MakeTypeError("proto_object_or_null", [proto]);
}
if (IS_SPEC_OBJECT(obj)) {
%SetPrototype(obj, proto); // MAKE IT FAST
}
return obj;
}
Que directamente está en Object
:
InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Entonces, hemos recorrido el camino desde el código que Petka escribió al desnudo. Esto fue lindo.
Renuncia:
Recuerde que esto es todo el detalle de implementación. Las personas como Petka son fanáticos de la optimización. Recuerde siempre que la optimización prematura es la raíz de todo mal el 97% del tiempo. Bluebird hace algo muy básico muy a menudo, por lo que gana mucho con estos hacks de rendimiento, ya que no es fácil llegar tan rápido como las devoluciones de llamada. Rara vez tiene que hacer algo como esto en un código que no enciende una biblioteca.