Подтвердить что ты не робот

Медленный вызов функции в V8 при использовании одной и той же клавиши для функций в разных объектах

Возможно, не потому, что вызов медленный, а скорее поиск; Я не уверен, но вот пример:

var foo = {};
foo.fn = function() {};

var bar = {};
bar.fn = function() {};

console.time('t');

for (var i = 0; i < 100000000; i++) {
    foo.fn();
}

console.timeEnd('t');

Протестировано на win8.1

  • firefox 35.01: ~ 240 мс
  • chrome 40.0.2214.93 (V8 3.30.33.15): ~ 760 мс
  • msie 11: 34 сек
  • nodejs 0.10.21 (V8 3.14.5.9): ~ 100 мс
  • iojs 1.0.4 (V8 4.1.0.12): ~ 760 мс

Теперь вот интересная часть, если я изменяю bar.fn на bar.somethingelse:

  • chrome 40.0.2214.93 (V8 3.30.33.15): ~ 100 мс
  • nodejs 0.10.21 (V8 3.14.5.9): ~ 100 мс
  • iojs 1.0.4 (V8 4.1.0.12): ~ 100 мс

Что-то пошло не так в v8 в последнее время? Что вызывает это?

4b9b3361

Ответ 1

Первые основы.

V8 использует скрытые классы, связанные с переходами, чтобы обнаружить статическую структуру в пушистых бесформенных объектах JavaScript.

Скрытые классы описывают структуру объекта, переходы соединяют скрытые классы вместе, описывая, какой скрытый класс следует использовать, если какое-то действие выполняется над объектом.

Например, приведенный ниже код приведет к следующей цепочке скрытых классов:

var o1 = {};
o1.x = 0;
o1.y = 1;
var o2 = {};
o2.x = 0;
o2.y = 0;

enter image description here

Эта цепочка создается при построении o1. Когда o2 построено, V8 просто следует за установленными переходами.

Теперь, когда свойство fn используется для хранения функции, V8 пытается дать этому свойству особое обращение: вместо того, чтобы просто объявлять в скрытом классе, что объект содержит свойство fn V8 , помещает функцию в скрытый класс.

var o = {};
o.fn = function fff() { };

enter image description here

Теперь есть интересное следствие: если вы сохраняете разные функции в поле с тем же именем, V8 больше не может просто следовать за переходами, потому что значение свойства функции не соответствует ожидаемому значению:

var o1 = {};
o1.fn = function fff() { };
var o2 = {};
o2.fn = function ggg() { };

При оценке o2.fn = ... присваивание V8 увидит, что есть переход с меткой fn, но он приводит к скрытому классу, который не подходит: он содержит свойство fff в fn, а мы пытаемся сохранить ggg. Примечание. Я дал имена функций только для простоты - V8 не использует их имена внутри себя, а их личность.

Поскольку V8 не может следовать этому переходу, V8 решит, что его решение о продвижении функции в скрытый класс было неправильным и расточительным. Изображение изменится

enter image description here

V8 создаст новый скрытый класс, где fn - это просто свойство, а не свойство постоянной функции. Он перенаправляет переход, а также отменяет устаревшую старую цель перехода. Помните, что o1 все еще использует его. Однако следующий код времени касается o1, например. когда свойство загружается из него - среда выполнения будет переносить o1 из устаревшего скрытого класса. Это делается для уменьшения полиморфизма - мы не хотим, чтобы o1 и o2 имели разные скрытые классы.

Почему важно иметь функции для скрытых классов? Потому что это дает V8 оптимизацию информации компилятора, которую он использует для встроенных вызовов методов. Он может обрабатывать только встроенный метод, если цель вызова хранится в самом скрытом классе.

Теперь применим это знание к приведенному выше примеру.

Так как существует столкновение между переходами bar.fn и foo.fn становятся нормальными свойствами - с функциями, хранящимися непосредственно на этих объектах, а V8 не может встроить вызов foo.fn, что приведет к более низкой производительности.

Может ли он вставить вызов раньше? Да. Вот что изменилось: в старшем V8 не было механизма отмены, поэтому даже после того, как мы столкнулись и перешли на fn переход, foo не был перенесен в скрытый класс, где fn становится нормальным свойством. Вместо этого foo сохранил скрытый класс, где fn - свойство постоянной функции, непосредственно встроенное в скрытый класс, позволяющий оптимизировать компилятор для его встроенного.

Если вы попытаетесь выбрать время bar.fn для более старого node, вы увидите, что он медленнее:

for (var i = 0; i < 100000000; i++) {
    bar.fn();  // can't inline here
}       

именно потому, что он использует скрытый класс, который не позволяет оптимизировать компилятор для встроенного вызова bar.fn.

Теперь последнее, что нужно заметить, это то, что этот тест не измеряет производительность вызова функции, а скорее измеряет, может ли оптимизатор компилятора уменьшить этот цикл до пустого цикла, введя в него вызов.

Ответ 2

Объектные литералы разделяют скрытый класс ( "карта" в внутренних терминах v8) по структуре I.E. одинаковые именованные ключи в том же порядке, в то время как объекты созданные из разных конструкторов, имели бы другой скрытый класс, даже если конструкторы инициализировали их точно в одни и те же поля.

При генерации кода для foo.fn() в компиляторе обычно нет доступа к конкретному объекту foo, а только к его скрытому классу. Из скрытого класса вы можете получить доступ к функции fn, но потому что общий скрытый класс может фактически иметь различную функцию в свойстве fn, что невозможно. Поэтому, поскольку во время компиляции вы не знаете, какая функция будет вызываться, вы не можете вставить вызов.

Если вы запустите код с флагом следа трассировки:

$ /c/etc/iojs.exe --trace-inlining test.js
t: 651ms

Однако если вы меняете что-либо, так что либо .fn всегда одна и та же функция, либо foo и bar имеют другой скрытый класс:

$ /c/etc/iojs.exe --trace-inlining test.js
Inlined foo.fn called from .
t: 88ms

(Я сделал это, выполнив bar.asd = 3 до bar.fn -ассоциирования, но есть много различных способов его достижения, таких как конструкторы и прототипы, которые вы наверняка знаете, - это путь для высокопроизводительного javascript )

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

var foo = {};
foo.fn = function() {};

var bar = {};
bar.fn = function() {};

foo.fn();
console.log("foo and bare share hidden class: ", %HaveSameMap(foo, bar));

Как вы можете видеть, результаты различаются между node10 и iojs:

$ /c/etc/iojs.exe --allow-natives-syntax test.js
foo and bare share hidden class:  true

$ node --allow-natives-syntax test.js
foo and bare share hidden class:  false

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

IE11 является закрытым источником, но из всего, что они документировали, на самом деле похоже, что он очень похож на v8.