¿Cómo la función util.toFastProperties de Bluebird hace que las propiedades de un objeto sean "rápidas"?

165

En el util.jsarchivo 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 de JSHint sobre esto:

'Eval' inalcanzable después de 'return'. (W027)

¿Qué hace exactamente esta función? ¿ util.toFastPropertiesRealmente hace que las propiedades de un objeto sean "más rápidas"?

He buscado en el repositorio GitHub de Bluebird cualquier comentario en el código fuente o una explicación en su lista de problemas, pero no pude encontrar ninguno.

Qantas 94 Heavy
fuente

Respuestas:

314

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 deleteenunciado 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 .prototypepropiedad 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.ccpodemos 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::MigrateSlowToFastsolo 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 SetPrototypeen 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 FuntionSetPrototypecuál es con lo que obtenemos .prototype =.

Hacerlo __proto__ =o .setPrototypeOftambié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 .setPrototypeOfpodemos 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.

Benjamin Gruenbaum
fuente
37
Esta es la publicación más interesante que he leído en mucho tiempo. Mucho respeto y aprecio a ti!
m59
2
@timoxley Escribí lo siguiente sobre eval(en los comentarios del código al explicar el código OP publicado): "evita que la función se optimice mediante la eliminación de código muerto u otras optimizaciones. Este código nunca se alcanza pero incluso el código inalcanzable hace que v8 no se optimice funciones ". . Aquí hay una lectura relacionada . ¿Quieres que profundice más en el tema?
Benjamin Gruenbaum
3
@dherman a 1;no causaría una "desoptimización", debugger;probablemente habría funcionado igualmente bien. Lo bueno es que cuando evalse pasa algo que no es una cuerda, no hace nada con él, por lo que es bastante inofensivo, algo así comoif(false){ debugger; }
Benjamin Gruenbaum
66
Por cierto, este código se ha actualizado debido a un cambio en la v8 reciente, ahora también debe crear una instancia del constructor. Entonces se volvió más perezoso; d
Esailija
44
@BenjaminGruenbaum ¿Puede explicar por qué esta función NO debe optimizarse? En el código minificado, eval de todos modos no está presente. ¿Por qué es útil evaluar aquí en el código no minificado?
Boopathi Rajaa