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

165

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

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

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

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

Недоступный 'eval' после 'return'. (W027)

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

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

Qantas 94 Heavy
источник

Ответы:

314

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

Бенджамин Грюнбаум
источник
37
Это самый интересный пост, который я когда-либо читал. Большое уважение и благодарность вам!
m59
2
@timoxley Я написал следующее о eval(в комментариях к коду при объяснении кода, опубликованного ОП): «не позволяйте оптимизировать функцию с помощью удаления мертвого кода или дальнейшей оптимизации. Этот код никогда не достигается, но даже недоступный код приводит к тому, что v8 не оптимизируется функции «. , Вот связанное чтение . Вы хотели бы, чтобы я более подробно остановился на этой теме?
Бенджамин Грюнбаум
3
@dherman a 1;не вызвал бы «деоптимизацию», a debugger;, вероятно, работал бы одинаково хорошо. Приятно то, что когда evalпередается что-то, что не является строкой, оно ничего не делает с этим, так что это довольно безопасно - вроде какif(false){ debugger; }
Бенджамин Грюнбаум
6
Кстати, этот код был обновлен из-за изменений в последней версии 8, теперь вам нужно создать экземпляр конструктора тоже. Так стало лениво; d
Esailija
4
@BenjaminGruenbaum Можете ли вы объяснить, почему эту функцию НЕ следует оптимизировать? В минимизированном коде eval в любом случае отсутствует. Почему eval полезен здесь в неминифицированном коде?
Бупати Раджаа