Ответ 1
Обновление 2017: во-первых, для читателей, которые идут сегодня - вот версия, которая работает с 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
}
Без одной или двух небольших оптимизаций - все ниже все еще действует.
Позвольте сначала обсудить, что он делает и почему это быстрее, а затем почему он работает.
Что он делает
В двигателе V8 используются два объекта:
- Режим словаря - в каком объекте хранятся как карты ключевых значений как хэш-карта.
- Быстрый режим - в котором объекты хранятся как structs, в которых нет вычислений, связанных с доступом к свойствам.
Вот простая демонстрация, которая демонстрирует разницу в скорости. Здесь мы используем оператор delete
, чтобы заставить объекты в режиме медленного словаря.
Двигатель пытается использовать быстрый режим, когда это возможно, и обычно всякий раз, когда выполняется большой доступ к свойствам, однако иногда он попадает в режим словаря. Быть в режиме словаря имеет большое ограничение производительности, поэтому, как правило, желательно быстро помещать объекты.
Этот хак предназначен для принудительного перевода объекта в быстрый режим из режима словаря.
- Сам Bluebird Petka рассказывает об этом здесь.
- Эти слайды Вячеслава Егорова также упоминают об этом.
- Этот вопрос и его принятый ответ также связаны.
- Эта слегка устаревшая статья по-прежнему является довольно хорошим чтением, которое может дать вам хорошую идею о том, как объекты хранятся в v8.
Почему это быстрее
В прототипах JavaScript обычно хранятся функции, разделяемые во многих случаях, и редко изменяются очень динамично. По этой причине очень желательно иметь их в быстром режиме, чтобы избежать дополнительного штрафа каждый раз, когда вызывается функция.
Для этого - v8 с радостью поместит объекты, являющиеся свойством .prototype
функций в быстром режиме, поскольку они будут совместно использоваться каждым объектом, созданным при вызове этой функции в качестве конструктора. Это, как правило, умная и желательная оптимизация.
Как это работает
Сначала пройдите через код и укажите, что делает каждая строка:
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.
}
Нам не нужно самим найти код, чтобы утверждать, что v8 делает эту оптимизацию, вместо этого мы можем читать модульные тесты 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));
Чтение и запуск этого теста показывает нам, что эта оптимизация действительно работает в v8. Однако - было бы неплохо посмотреть, как это сделать.
Если мы проверим objects.cc
, мы найдем следующую функцию (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);
}
}
Теперь JSObject::MigrateSlowToFast
просто явно использует словарь и преобразует его в быстрый объект V8. Это достойное чтение и интересное понимание внутренних объектов V8, но это не предмет. Я по-прежнему горячо рекомендую что вы читаете его здесь, так как это хороший способ узнать об объектах v8.
Если мы проверим SetPrototype
в objects.cc
, мы увидим, что он вызывается в строке 12231:
if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Что, в свою очередь, называется FuntionSetPrototype
, что мы получаем с помощью .prototype =
.
Выполнение __proto__ =
или .setPrototypeOf
также могло бы работать, но это функции ES6, а Bluebird запускается во всех браузерах с Netscape 7, поэтому здесь нет необходимости упрощать код. Например, если мы проверим .setPrototypeOf
, мы можем видеть:
// 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;
}
Что непосредственно находится на Object
:
InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Итак - мы прошли путь от кода, который Петька написал на голом металле. Это было приятно.
Отказ от ответственности:
Помните, что это все детали реализации. Такие люди, как Петька, - оптимизаторы. Всегда помните, что преждевременная оптимизация - это корень всех злых 97% времени. Bluebird делает что-то очень основное очень часто, так что он получает много от этих хакеров производительности - так быстро, как обратные вызовы, непросто. Вам редко приходится делать что-то подобное в коде, не использующем библиотеку.