Почему Function.prototype.bind работает медленно?

При сравнении этого теста с Chrome 16 и Opera 11.6 мы обнаруживаем, что

  • в chrome native bind работает почти в 5 раз медленнее, чем эмулированная версия bind
  • в опере нативная привязка работает почти в 4 раза быстрее, чем эмулированная версия привязки

Где эмулированная версия bind в этом случае

var emulatebind = function (f, context) {
    return function () {
        f.apply(context, arguments);
    };
};

Есть ли веские причины, по которым существует такая разница, или это просто вопрос недостаточной оптимизации v8?

Примечание: этот emulatebind реализует только подмножество, но на самом деле это не имеет значения. Если у вас есть полнофункциональная и оптимизированная эмулируемая привязка, разница в производительности в тесте все еще существует.


person Raynos    schedule 28.12.2011    source источник
comment
@RobW встроил эмулированную версию bind, с которой я ее сравниваю.   -  person Raynos    schedule 28.12.2011
comment
Полагаю, это связано с разной оптимизацией кода. Возможно, обертка с нативным биндом не допускает некоторых оптимизаций. FF10 демонстрирует похожее поведение.   -  person kirilloid    schedule 28.12.2011
comment
Ваш вопрос должен быть следующим: Почему моя эмулированная функция .bind() работает быстрее, чем нативная в Chrome, FireFox и медленнее в Opera и IE?. А почему вы думаете что должно быть иначе? Различная оптимизация кода. Ваш эмулируемый бинд не позволяет добавлять параметры, а только контекст, например.   -  person Andrew D.    schedule 28.12.2011
comment
@ЭндрюД. в основном да. Я хочу знать конкретные оптимизации движка, которые объясняют эту конкретную разницу.   -  person Raynos    schedule 28.12.2011
comment
Вот дополнение к вашему тестовому примеру, в котором используется версия es5-shim: jsperf.com/bind-vs -подражать/6   -  person Domenic    schedule 07.01.2012


Ответы (3)


На основе 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);
}

(ссылка в виде открытого текста на v8natives.js на случай прекращения работы Google Code Search.)

Немного озадачивает тот факт, что, по крайней мере, для Chrome 16 версия es5-shim по-прежнему работает быстрее, чем нативная версия. И что другие браузеры имеют довольно разные результаты для es5-shim по сравнению с родным. Предположение: возможно, %_IsConstructCall() даже медленнее, чем this instanceof bound, возможно, из-за пересечения границ нативного/JS-кода. И, возможно, другие браузеры имеют гораздо более быстрый способ проверки вызова [[Construct]].

person Domenic    schedule 06.01.2012
comment
добавлена ​​проверка instanceof в raynosBound. Я думаю, что накладные расходы в основном связаны с объединением пустых массивов. Как вы думаете, стоит ли вручную оптимизировать прокладку привязки ES5 для случая args.length === 0, чтобы она возвращала функцию, которая просто выполняет target.apply(that, arguments)? - person Raynos; 07.01.2012
comment
Лично я думаю, что любая оптимизация здесь является микрооптимизацией, и я бы подождал, пока мои тесты не покажут, что связанные функции опережают, например. сетевая задержка из-за того, что мое приложение работает медленно, прежде чем рассматривать что-либо в этом роде. - person Domenic; 07.01.2012

Невозможно реализовать полнофункциональный bind только на ES5. В отдельных разделах с 15.3.4.5.1 по 15.3.4.5.3 спецификации нельзя эмулировать.

15.3.4.5.1, в частности, кажется возможной нагрузкой на производительность: в функциях с короткой привязкой есть разные [[Call]] внутренние свойства, поэтому их вызов, скорее всего, займет необычный и, возможно, более сложный путь кода.

Различные другие специфические неэмулируемые функции связанной функции (такие как отравление arguments/caller и, возможно, пользовательская length, независимая от исходной подписи) могут добавить накладные расходы на каждый вызов, хотя я допускаю, что это немного маловероятно. Хотя похоже, что V8 даже не реализует отравление на данный момент.

ИЗМЕНИТЬ этот ответ является предположением, но мой другой ответ содержит нечто более близкое к доказательству. Я все еще думаю, что это правильное предположение, но это отдельный ответ, поэтому я оставлю его как таковой и просто отошлю вас к другому.

person Domenic    schedule 06.01.2012

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

person Mike Samuel    schedule 06.01.2012
comment
Он не полностью эмулирует привязку. Но тесты производительности используют только те части bind, которые эмулируются. Можете ли вы объяснить, почему каррирование параметров приводит к накладным расходам, если они не используются в тестах производительности? Можете ли вы редактировать тесты с полнофункциональным? - person Raynos; 06.01.2012
comment
@Raynos, добавил ссылку на исходный код JS для функции привязки от v8natives.js. - person Mike Samuel; 06.01.2012
comment
Хм, почему бы не var newArgs = [].slice.call( arguments );, а потом f.apply( context, curriedArgs.concat( newArgs ) );... - person Šime Vidas; 06.01.2012
comment
Вы связали модульные тесты, а не исходный код реализации. Полезно связать оба. - person Raynos; 06.01.2012
comment
@Raynos, вторая ссылка на юнит-тесты. Во-первых, это реализация: google.com/codesearch# W9JxUuHYyMg/trunk/src/ - person Mike Samuel; 07.01.2012
comment
@Райнос. В этой реализации есть ошибка: var curriedArgs = Array.prototype.slice.call(arguments, 2); должно быть var curriedArgs = Array.prototype.slice.call(arguments, 1); - person ansiart; 14.01.2014
comment
@ansiart, должно ли thisValue/context быть частью curriedArgs? - person Mike Samuel; 14.01.2014
comment
Я просто имел в виду одну конкретную строку в ссылке, которую он разместил: jsperf.com/bind- vs-эмуляция/4 - person ansiart; 14.01.2014
comment
Кстати, спасибо @Raynos, это самая быстрая реализация, которую я когда-либо видел. - person ansiart; 14.01.2014