Почему новый медленный?
Тест:
JsPerf
Инварианты:
var f = function() { };
var g = function() { return this; }
Тесты:
Ниже в порядке ожидаемой скорости
-
new f;
-
g.call(Object.create(Object.prototype));
-
new (function() { })
-
(function() { return this; }).call(Object.create(Object.prototype));
Фактическая скорость:
-
new f;
-
g.call(Object.create(Object.prototype));
-
(function() { return this; }).call(Object.create(Object.prototype));
-
new (function() { })
Вопрос:
- Когда вы меняете
f
и g
на встроенные анонимные функции. Почему тест new
(тест 4.) медленнее?
Update:
Что конкретно заставляет new
быть медленнее, когда f
и g
являются встроенными.
Мне интересны ссылки на спецификацию ES5 или ссылки на исходный код JagerMonkey или V8. (Не стесняйтесь связывать АО и Караканский исходный код. О, и команда IE может течь источник Чакры, если они хотят).
Если вы связываете источник JS-движка, объясните его.
Ответы
Ответ 1
Проблема в том, что вы можете проверить текущий исходный код различных движков, но это вам не поможет. Не пытайтесь перехитрить компилятор. В любом случае они попытаются оптимизировать для наиболее распространенного использования. Я не думаю, что (function() { return this; }).call(Object.create(Object.prototype))
, называемый 1000 раз, имеет реальный случай использования вообще.
"Программы должны быть написаны для людей, чтобы читать, и только случайно для машин для выполнения".
Абельсон и Суссман, SICP, предисловие к первому изданию
Ответ 2
Основное различие между # 4 и всеми другими случаями заключается в том, что в первый раз, когда вы используете закрытие в качестве конструктора, всегда довольно дорого.
-
Он всегда обрабатывается во время выполнения V8 (не в сгенерированном коде), и переход между скомпилированным кодом JS и временем выполнения С++ довольно дорог. Последующие распределения обычно обрабатываются в сгенерированном коде. Вы можете взглянуть на Generate_JSConstructStubHelper
в builtins-ia32.cc
и заметить, что он падает до Runtime_NewObject
, когда закрытие не имеет начальной карты. (см. http://code.google.com/p/v8/source/browse/trunk/src/ia32/builtins-ia32.cc#138)
-
Когда закрытие используется в качестве конструктора в первый раз, V8 должен создать новую карту (aka hidden class) и назначить ее как начальную карту для этого закрытия. См. http://code.google.com/p/v8/source/browse/trunk/src/heap.cc#3266. Важно отметить, что карты выделяются в отдельном пространстве памяти. Это пространство не может быть очищено быстрым сборщиком частичной уборки. Когда переполнение пространства карты V8 должно выполнять относительно дорогие полные GC-метки.
Есть несколько других вещей, которые случаются, когда вы впервые используете закрытие в качестве конструктора, но 1 и 2 являются основными факторами медленности тестового примера № 4.
Если мы сравниваем выражения # 1 и # 4, то различия заключаются в следующем:
-
1 не выделяет новое замыкание каждый раз;
-
1 не вводит время выполнения каждый раз: после закрытия получает начальную конструкцию карты обрабатывается в быстром пути сгенерированного кода. Обработка всей конструкции в сгенерированном коде намного быстрее, чем повторение между временем выполнения и сгенерированным кодом;
-
1 не выделяет новую начальную карту для каждого нового закрытия каждый раз;
-
1 не вызывает разметки с помощью переполнения пространства карты (только дешевые зачистки).
Если сравнить # 3 и # 4, то отличия:
-
3 не выделяет новую начальную карту для каждого нового закрытия каждый раз;
-
3 не вызывает разметки с помощью переполнения пространства карты (только дешевые зачистки);
-
4 делает меньше на стороне JS (no Function.prototype.call, no Object.create, no Object.prototype lookup и т.д.) больше на стороне С++ (# 3 также входит во время выполнения каждый раз, когда вы делаете Object.create, но там очень мало).
Нижняя строка здесь заключается в том, что в первый раз, когда вы используете закрытие, поскольку конструктор дорог по сравнению с последующими строительными вызовами одного и того же закрытия, потому что V8 должен установить некоторую сантехнику. Если мы немедленно отбросим закрытие, мы в основном выбросим всю работу V8, чтобы ускорить последующие вызовы конструктора.
Ответ 3
Я предполагаю, что следующие расширения объясняют, что происходит в V8:
Ответ 4
Ну, эти два звонка не делают точно то же самое. Рассмотрим этот случай:
var Thing = function () {
this.hasMass = true;
};
Thing.prototype = { holy: 'object', batman: '!' };
Thing.prototype.constructor = Thing;
var Rock = function () {
this.hard = 'very';
};
Rock.prototype = new Thing();
Rock.constructor = Rock;
var newRock = new Rock();
var otherRock = Object.create(Object.prototype);
Rock.call(otherRock);
newRock.hard // => 'very'
otherRock.hard // => 'very'
newRock.hasMass // => true
otherRock.hasMass // => undefined
newRock.holy // => 'object'
otherRock.holy // => undefined
newRock instanceof Thing // => true
otherRock instanceof Thing // => false
Итак, мы можем видеть, что вызов Rock.call(otherRock)
не вызывает otherRock
для наследования из прототипа. Это должно учитывать, по крайней мере, некоторую добавленную медленность. Хотя в моих тестах конструкция new
почти в 30 раз медленнее, даже в этом простом примере.
Ответ 5
new f;
- взять локальную функцию 'f' (доступ по
индекс в локальном фрейме) - дешево.
- выполнить bytecode BC_NEW_OBJECT (или
что-то подобное) - дешево.
- Выполните функцию - дешево здесь.
Теперь это:
g.call(Object.create(Object.prototype));
- Найти глобальный var
Object
- дешево?
- Найти свойство
prototype
в Object - so-so
- Найти свойство
create
в Object - so-so
- Найти локальный var g; - дешевый
- Найти свойство
call
- so-so
- Вызывать функцию
create
- так себе
- Вызывать функцию
call
- так себе
И это:
new (function() { })
- создать новый объект функции (эта анонимная функция) -
относительно дорого.
- выполнить bytecode BC_NEW_OBJECT -
дешевые
- Выполните функцию - дешево здесь.
Как вы видите, случай №1 наименее потребляет.