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 delete
instruçã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 .prototype
propriedade 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::MigrateSlowToFast
apenas 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- SetPrototype
in 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 FuntionSetPrototype
qual é o que obtemos .prototype =
.
Fazer __proto__ =
ou .setPrototypeOf
teria 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.