Обновление 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 использует два представления объекта:
- Режим словаря - в котором объект хранится в виде карт ключ-значение в виде хэш-карты .
- Быстрый режим - в котором объекты хранятся как структуры , в которых нет доступа к вычислениям, связанным с доступом к свойству.
Вот простая демонстрация, которая демонстрирует разницу в скорости. Здесь мы используем delete
оператор для перевода объектов в режим медленного словаря.
Движок пытается использовать быстрый режим всякий раз, когда это возможно, и обычно всякий раз, когда выполняется большой доступ к свойству, однако иногда он переходит в режим словаря. Нахождение в режиме словаря влечет за собой большие потери производительности, поэтому обычно желательно переводить объекты в быстрый режим.
Этот хак предназначен для перевода объекта в быстрый режим из словарного режима.
Почему это быстрее
В 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 очень часто делает что-то очень простое, поэтому она очень выигрывает от этих хаков производительности - быть быстрым, как обратные вызовы, нелегко. Вам редко приходится делать что-то подобное в коде, который не питает библиотеку.
eval
(в комментариях к коду при объяснении кода, опубликованного ОП): «не позволяйте оптимизировать функцию с помощью удаления мертвого кода или дальнейшей оптимизации. Этот код никогда не достигается, но даже недоступный код приводит к тому, что v8 не оптимизируется функции «. , Вот связанное чтение . Вы хотели бы, чтобы я более подробно остановился на этой теме?1;
не вызвал бы «деоптимизацию», adebugger;
, вероятно, работал бы одинаково хорошо. Приятно то, что когдаeval
передается что-то, что не является строкой, оно ничего не делает с этим, так что это довольно безопасно - вроде какif(false){ debugger; }