Como a função util.toFastProperties do Bluebird torna as propriedades de um objeto "rápidas"?

165

No util.jsarquivo do Bluebird , ele tem a seguinte função:

function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);
}

Por alguma razão, há uma declaração após a função de retorno, que não sei por que ela está lá.

Além disso, parece que é deliberado, pois o autor havia silenciado o aviso do JSHint sobre isso:

'Eval' inacessível após 'retorno'. (W027)

O que exatamente essa função faz? Será que util.toFastPropertiesrealmente fazer propriedades de um objeto "mais rápido"?

Pesquisei no repositório GitHub do Bluebird por comentários no código fonte ou uma explicação na lista de problemas, mas não encontrei nenhum.

Qantas 94 Heavy
fonte

Respostas:

314

Atualização de 2017: primeiro, para os leitores que estão chegando hoje - aqui está uma versão que funciona com o Nó 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
}

Sem uma ou duas pequenas otimizações - todas as opções abaixo ainda são válidas.

Vamos primeiro discutir o que faz e por que é mais rápido e depois por que funciona.

O que faz

O mecanismo V8 usa duas representações de objetos:

  • Modo Dicionário - no qual o objeto é armazenado como mapas de valores-chave como um mapa de hash .
  • Modo rápido - no qual os objetos são armazenados como estruturas , nos quais não há computação envolvida no acesso à propriedade.

Aqui está uma demonstração simples que demonstra a diferença de velocidade. Aqui usamos a deleteinstrução para forçar os objetos no modo de dicionário lento.

O mecanismo tenta usar o modo rápido sempre que possível e geralmente sempre que um grande acesso à propriedade é realizado - no entanto, às vezes, é lançado no modo de dicionário. Estar no modo dicionário tem uma grande penalidade de desempenho; portanto, geralmente é desejável colocar objetos no modo rápido.

Esse hack destina-se a forçar o objeto no modo rápido a partir do modo de dicionário.

Por que é mais rápido

Em protótipos JavaScript, normalmente armazenam funções compartilhadas entre muitas instâncias e raramente mudam muito dinamicamente. Por esse motivo, é muito desejável tê-los no modo rápido para evitar a penalidade extra toda vez que uma função é chamada.

Para isso, a v8 terá prazer em colocar objetos que são .prototypepropriedade de funções no modo rápido, pois serão compartilhados por todos os objetos criados ao invocar essa função como construtor. Geralmente, essa é uma otimização inteligente e desejável.

Como funciona

Vamos primeiro analisar o código e descobrir o que cada linha faz:

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.
}

Não precisamos encontrar o código para afirmar que a v8 faz essa otimização; em vez disso, podemos ler os testes de unidade da 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));

A leitura e a execução deste teste nos mostram que essa otimização realmente funciona na v8. No entanto - seria bom ver como.

Se verificarmos objects.cc, podemos encontrar a seguinte função (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);
  }
}

Agora, JSObject::MigrateSlowToFastapenas pega explicitamente o Dicionário e o converte em um objeto V8 rápido. É uma leitura interessante e uma visão interessante sobre os objetos internos da v8 - mas não é o assunto aqui. Eu ainda recomendo que você leia aqui , pois é uma boa maneira de aprender sobre os objetos da v8.

Se fizermos check- SetPrototypein objects.cc, podemos ver que ele é chamado na linha 12231:

if (value->IsJSObject()) {
    JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}

Que, por sua vez, é chamado pelo FuntionSetPrototypequal é o que obtemos .prototype =.

Fazer __proto__ =ou .setPrototypeOfteria funcionado, mas essas são as funções do ES6 e o ​​Bluebird é executado em todos os navegadores desde o Netscape 7, o que está fora de questão para simplificar o código aqui. Por exemplo, se verificarmos .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;
}

Qual diretamente está em Object:

InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));

Então - nós seguimos o caminho do código que Petka escreveu para o metal puro. Isso foi legal.

Aviso Legal:

Lembre-se de que isso é todo detalhe da implementação. Pessoas como Petka são loucos por otimização. Lembre-se sempre de que a otimização prematura é a raiz de todos os males 97% das vezes. O Bluebird faz algo muito básico com muita frequência, por isso ganha muito com esses hacks de desempenho - ser tão rápido quanto os retornos de chamada não é fácil. Você raramente precisa fazer algo assim no código que não alimenta uma biblioteca.

Benjamin Gruenbaum
fonte
37
Este é o post mais interessante que já li há algum tempo. Muito respeito e apreço a você!
M59
2
@timoxley Eu escrevi o seguinte sobre o eval(nos comentários do código ao explicar o código OP publicado): "impedir que a função seja otimizada através da eliminação do código morto ou de otimizações adicionais. Esse código nunca é alcançado, mas mesmo o código inacessível faz com que a v8 não otimize funções." . Aqui está uma leitura relacionada . Deseja que eu elabore mais sobre o assunto?
Benjamin Gruenbaum 28/07
3
@herman a 1;não causaria uma "desoptimização", a debugger;provavelmente teria funcionado igualmente bem. O bom é que, quando evalé passado algo que não é uma string que não fazer nada com ele, por isso é bastante inofensivo - tipo comoif(false){ debugger; }
Benjamin Gruenbaum
6
Btw este código foi atualizado devido a uma alteração na v8 recente, agora você precisa instanciar o construtor também. Então ficou mais preguiçoso; d
Esailija 29/03
4
@BenjaminGruenbaum Você pode explicar por que essa função NÃO deve ser otimizada? De qualquer forma, no código minificado, eval não está presente. Por que eval é útil aqui no código não minificado?
Boopathi Rajaa