Почему вы вызываете метод на литеральном объекте медленнее на V8?

Я был удивлен результатами этого простого теста jsperf:

Benchmark.prototype.setup = function() {
  var O = function() {
      this.f = function(){};
  }
  var o = new O();
  var o2 = {
      f : function(){}
  };
};

// Test case #1
o.f();  // ~721M ops/s

// Test case #2
o2.f(); // ~135M ops/s

Я ожидал, что и то, и другое (и на самом деле производительность аналогична в Firefox). V8 должен оптимизировать что-то на примере # 1, но что?

Ответ 1

Первые основы о V8 и jsPerf:

  • В V8 используется метод, называемый скрытыми классами. Каждый скрытый класс описывает определенную форму объекта, например. объект имеет свойство x при смещении 16 или объект имеет метод f, и эти скрытые классы связаны вместе с переходами, поскольку объект мутирован, образуя деревья перехода (которые, строго говоря, dags). Не все скрытые классы находятся в одном и том же дереве перехода; вместо этого из каждого конструктора берется новое дерево перехода. Посмотрите эти слайды, чтобы понять основную идею скрытых классов.

  • Когда jsPerf выполняет следующие тесты: заданные setup и body, он несколько раз генерирует и запускает функцию, выглядящую примерно так:

    function measure() {
      /* setup */
      var start = Date.now();
      for (var i = 0; i < N; i++) {
        /* body */
      }
      var end = Date.now();
    
      /* N / (start - end) determines ops / ms reported */
    }
    

    Каждый запуск называется образцом.

Теперь давайте взглянем на деревья перехода в вашем тесте.

  • Скрытый класс o принадлежит дереву перехода с корнем в конструкторе o. Обратите внимание, что каждый конструктор вызывается один раз. Это позволяет V8 строить следующее дерево перехода в памяти:

    O{} -f-> O{ f: <closure> }
    

    Скрытый класс o по существу говорит V8, что o имеет метод, называемый f, реализованный данным закрытием. Это позволяет компилятору, оптимизирующему V8, встроить f в ваш тест выше, что по существу делает цикл бенчмаркинга пустым.

  • Скрытый класс o2 принадлежит дереву перехода Object. Сначала обратите внимание, что setup вызывается несколько раз, поэтому, если V8 попытался применить ту же оптимизацию с продвижением f к методу, который он прибудет в дерево невозможного перехода:

    Object{} -f-> Object{ f: <closure> }
             -f-> Object{ f: <other closure> }
    

    На самом деле V8 даже не пытается. Разработчики V8 предвидели эту ситуацию, и V8 просто делает f нормальным свойством:

    Object{} -f-> Object{ f: <property at offset 8> }
    

    Таким образом, для вызова o2.f() ему необходимо сначала загрузить его, и это также ухудшает вложение.

Здесь вы должны понимать одну важную вещь: если вы дважды вызываете конструктор o, тогда V8 прибудет в одно и то же дерево невозможного перехода, которое V8 избегает удара для Object:

    O{} -f-> O{ f: <closure> }
        -f-> O{ f: <other closure> }

У вас не может быть такой структуры. В этом случае V8 на лету преобразует f в поле вместо того, чтобы сделать его методом и переписывает дерево перехода:

    O{} -f-> O{ f: <property at offset 8> }

Смотрите этот эффект в http://jsperf.com/function-call-on-js-objects/3, где я добавил один new O(), прежде чем создавать o. Вы заметите, что производительность объектного литерала и объекта, построенного с помощью new, одинаковы.

Еще одна деталь заключается в том, что V8 попытается превратить f в метод для литерала, если литерал объявлен в глобальной области. См. http://jsperf.com/function-call-on-js-objects/5 и Проблема 2246 против V8. Обоснование здесь простое: литерал в глобальной области оценивается только один раз, поэтому вероятность того, что такое продвижение будет успешным, и не будет конфликтов между методами, которые возникли бы, если литерал оценивается несколько раз.

Вы можете узнать больше о похожих проблемах в моем сообщении в блоге.

Ответ 2

V8 делает оптимизацию для известных прототипов. Другими словами, оптимизируется использование и создание объектов с помощью new.

Вы можете написать более похожие тесты, и это всегда будет заключение.

Во втором случае вы ослепляете двигатель. Он не знает, почему, если или когда o2 будет иметь атрибут.