Как функция Bluebird util.toFastProperties делает свойства объекта "быстрыми"?

В файле Bluebird util.js он выполняет следующие функции:

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

По какой-то причине существует инструкция после функции возврата, и я не уверен, почему она там.

Кроме того, кажется, что это преднамеренно, поскольку автор отключил предупреждение JSHint об этом:

Недопустимый "eval" после "return". (W027)

Что именно делает эта функция? Действительно ли util.toFastProperties делает свойства объекта "быстрее"?

Я искал в репозитории Bluebird GitHub любые комментарии в исходном коде или объяснение в их списке проблем, но я не мог найти их.

Ответ 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, чтобы заставить объекты в режиме медленного словаря.

Двигатель пытается использовать быстрый режим, когда это возможно, и обычно всякий раз, когда выполняется большой доступ к свойствам, однако иногда он попадает в режим словаря. Быть в режиме словаря имеет большое ограничение производительности, поэтому, как правило, желательно быстро помещать объекты.

Этот хак предназначен для принудительного перевода объекта в быстрый режим из режима словаря.

Почему это быстрее

В прототипах 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 делает что-то очень основное очень часто, так что он получает много от этих хакеров производительности - так быстро, как обратные вызовы, непросто. Вам редко приходится делать что-то подобное в коде, не использующем библиотеку.