Почему Function.prototype.bind медленный?
Сравнивая этот тест с хром 16 и опера 11.6, мы обнаруживаем, что
- в хром native bind почти в 5 раз медленнее, чем эмулированная версия bind
- в оперативной связи bind почти в 4 раза быстрее, чем эмулированная версия bind
Если эмулированная версия привязки в этом случае
var emulatebind = function (f, context) {
return function () {
f.apply(context, arguments);
};
};
Есть ли веские причины, по которым существует такая разница, или это просто вопрос v8, недостаточно оптимизирующий?
Примечание: emulatebind
реализует только подмножество, но это не имеет особого значения. Если у вас полнофункциональное и оптимизированное эмулированное связывание, разница в производительности в эталонном файле все еще существует.
Ответы
Ответ 1
На основе http://jsperf.com/bind-vs-emulate/6, который добавляет версию es5-shim для сравнения, похоже, что виновником является дополнительная ветвь и instanceof
, которую должна выполнить связанная версия, чтобы проверить, вызвана ли она как конструктор.
Каждый раз, когда запущена связанная версия, исполняемый код по существу:
if (this instanceof bound) {
// Never reached, but the `instanceof` check and branch presumably has a cost
} else {
return target.apply(
that,
args.concat(slice.call(arguments))
);
// args is [] in your case.
// So the cost is:
// * Converting (empty) Arguments object to (empty) array.
// * Concating two empty arrays.
}
В исходном коде V8 эта проверка появляется (внутри boundFunction
) как
if (%_IsConstructCall()) {
return %NewObjectFromBound(boundFunction);
}
(ссылка Plaintext на v8natives.js, когда Google Code Search умирает.)
Немного озадачивает, что, по крайней мере, для Chrome 16 версия es5-shim все еще быстрее, чем родная версия. И что другие браузеры имеют довольно разные результаты для es5-shim против native. Спекуляция: может быть, %_IsConstructCall()
еще медленнее, чем this instanceof bound
, возможно, из-за пересечения границ родного/JS-кода. И, возможно, другие браузеры имеют гораздо более быстрый способ проверки вызова [[Construct]]
.
Ответ 2
Невозможно реализовать полнофункциональный bind
только в ES5. В частности, разделы 15.3.4.5.1 до 15.3.4.5.3 спецификации не могут быть эмулированы.
15.3.4.5.1, в частности, похоже на возможное бремя производительности: в коротких связанных функциях существуют разные внутренние свойства [[Call]]
, поэтому их вызов, скорее всего, приведет к необычному и, возможно, более сложному коду.
Различные другие неэмулируемые функции связанной функции (например, отравление arguments
/caller
и, возможно, пользовательский length
, не зависящий от исходной подписи), могут добавить накладные расходы для каждого вызова, хотя я допускаю это немного маловероятно. Хотя похоже, что V8 даже не реализует отравление на данный момент.
РЕДАКТИРОВАТЬ, этот ответ - это предположение, но мой другой ответ имеет нечто большее приближающееся доказательство. Я все еще думаю, что это правильное предположение, но это отдельный ответ, поэтому я оставлю его как таковой и просто отсылаю вас к другому.
Ответ 3
исходный код V8 для привязки реализован в JS.
OP не эмулирует bind
, потому что он не аргументирует аргументы способом bind
. Вот полнофункциональный bind
:
var emulatebind = function (f, context) {
var curriedArgs = Array.prototype.slice.call(arguments, 2);
return function () {
var allArgs = curriedArgs.slice(0);
for (var i = 0, n = arguments.length; i < n; ++i) {
allArgs.push(arguments[i]);
}
return f.apply(context, allArgs);
};
};
Очевидно, что быстрая оптимизация заключалась в том, чтобы сделать
return f.apply(context, arguments);
вместо curriedArgs.length == 0
, потому что в противном случае у вас есть два ненужных создания массива и ненужная копия, но, возможно, родная версия действительно реализована в JS и не делает эту оптимизацию.
Предостережение. Этот полнофункциональный bind
неправильно обрабатывает некоторые угловые случаи вокруг принуждения аргумента this
в строгом режиме. Это может быть еще одним источником накладных расходов.