Actualización de 2017: Primero, para los lectores que vienen hoy: aquí hay una versión que funciona con el Nodo 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 siguiente sigue siendo válido.
Primero discutamos 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 el que los objetos se almacenan como mapas de valores clave como un mapa hash .
- Modo rápido : en el que los objetos se almacenan como estructuras , en el que no hay cómputo involucrado en el acceso a la propiedad.
Aquí hay una demostración simple que demuestra la diferencia de velocidad. Aquí usamos el delete
enunciado para forzar los objetos al modo de diccionario lento.
El motor intenta utilizar el modo rápido siempre que sea posible y, en general, cada vez que se realiza una gran cantidad de acceso a la propiedad; sin embargo, a veces se pone en modo diccionario. Estar en modo diccionario tiene una gran penalización de rendimiento, por lo que generalmente es deseable poner objetos en modo rápido.
Este truco está destinado a forzar el objeto a modo rápido desde el modo diccionario.
¿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 deseable tenerlos en modo rápido para evitar la penalización adicional cada vez que se llama a una función.
Para esto, v8 con mucho gusto pondrá objetos que son .prototype
propiedad de funciones en modo rápido, ya que serán compartidos por cada objeto creado al invocar esa función como un constructor. Esta es generalmente una optimización inteligente y deseable.
Cómo funciona
Primero veamos el código y calculemos 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 realiza esta optimización, sino que podemos leer las pruebas unitarias de 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));
Leer y ejecutar esta prueba nos muestra que esta optimización de hecho funciona en v8. Sin embargo, sería bueno ver cómo.
Si verificamos 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
solo 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 los elementos internos de v8, pero no es el tema aquí. Todavía te recomiendo que lo leas aquí, ya que es una buena manera de aprender sobre los objetos v8.
Si nos la salida SetPrototype
en objects.cc
, podemos ver que se le llama en la línea 12231:
if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Lo que a su vez se llama por FuntionSetPrototype
cuál es con lo que obtenemos .prototype =
.
Hacerlo __proto__ =
o .setPrototypeOf
también hubiera funcionado, pero estas son funciones de ES6 y Bluebird se ejecuta en todos los navegadores desde Netscape 7, por lo que es imposible simplificar el código aquí. Por ejemplo, si verificamos .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 está directamente en Object
:
InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Entonces, hemos recorrido el camino desde el código que Petka escribió hasta el metal desnudo. Esto estuvo bien.
Descargo de responsabilidad:
Recuerde que esto es todo 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 obtiene mucho de estos hacks de rendimiento: ser tan rápido como las devoluciones de llamada no es fácil. Usted rara vez se tiene que hacer algo como esto en código que no se enciende una biblioteca.