Почему связывание происходит медленнее, чем закрытие?

79

Предыдущий постер спрашивал Function.bind vs Closure в Javascript: как выбрать?

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

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

Используя bind, вы вызываете функцию с существующей областью видимости, поэтому обход области видимости не выполняется.

Два jsperfs предполагают, что связывание на самом деле намного медленнее, чем закрытие .

Это было опубликовано как комментарий к приведенному выше

И я решил написать свой собственный jsperf

Так почему же связывание происходит намного медленнее (70 +% хрома)?

Так как это не быстрее и закрытия могут служить той же цели, следует ли избегать связывания?

Павел
источник
10
«Следует избегать связывания» - если вы не делаете это тысячу раз на странице - вам это не нужно.
zerkms
1
Для сборки асинхронной сложной задачи из небольших частей может потребоваться что-то похожее на это в nodejs, потому что обратные вызовы нужно как-то выровнять.
Пол
Думаю, это потому, что браузеры не приложили столько усилий для его оптимизации. См. Код Mozilla ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), чтобы реализовать его вручную. Есть все шансы, что браузеры просто делают это изнутри, а это гораздо больше, чем быстрое закрытие.
Дэйв
1
Косвенные вызовы функций ( apply/call/bind) в целом намного медленнее, чем прямые.
georg
@zerkms И кто скажет, что никто не делает этого тысячи раз? Я думаю, вы удивитесь, насколько часто это может быть благодаря функциональности, которую он предоставляет.
Эндрю

Ответы:

142

Обновление Chrome 59: как я и предсказывал в ответе ниже, привязка больше не работает медленнее с новым оптимизирующим компилятором. Вот код с подробностями: https://codereview.chromium.org/2916063002/

В большинстве случаев это не имеет значения.

Если вы не создаете приложение, в котором .bindесть узкое место, я бы не стал беспокоиться. В большинстве случаев удобочитаемость гораздо важнее чистой производительности. Я думаю, что использование нативного .bindкода обычно обеспечивает более читаемый и поддерживаемый код, что является большим плюсом.

Однако да, когда это важно - .bindмедленнее

Да, .bindэто значительно медленнее, чем закрытие - по крайней мере, в Chrome, по крайней мере, в том виде, в котором оно реализовано в настоящее время v8. Мне лично приходилось несколько раз переключаться на Node.JS из-за проблем с производительностью (в более общем плане, в ситуациях с высокой производительностью закрытие происходит медленно).

Почему? Поскольку .bindалгоритм намного сложнее, чем обертывание функции другой функцией и использование .callили .apply. (Интересный факт, он также возвращает функцию с toString, установленным на [native function]).

Есть два способа взглянуть на это с точки зрения спецификации и с точки зрения реализации. Давайте наблюдать за обоими.

Во-первых, давайте посмотрим на алгоритм привязки, определенный в спецификации :

  1. Пусть Target будет значением this.
  2. Если IsCallable (Target) имеет значение false, выбросить исключение TypeError.
  3. Пусть A будет новым (возможно, пустым) внутренним списком всех значений аргументов, указанных после thisArg (arg1, arg2 и т. Д.), По порядку.

...

(21. Вызовите внутренний метод [[DefineOwnProperty]] для F с аргументами «arguments», PropertyDescriptor {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable] » ]: false} и false.

(22. Вернуть Ф.

Кажется довольно сложным, намного больше, чем просто обертка.

Во-вторых, давайте посмотрим, как это реализовано в Chrome .

Давайте проверим FunctionBindисходный код v8 (chrome JavaScript engine):

function FunctionBind(this_arg) { // Length is 1.
  if (!IS_SPEC_FUNCTION(this)) {
    throw new $TypeError('Bind must be called on a function');
  }
  var boundFunction = function () {
    // Poison .arguments and .caller, but is otherwise not detectable.
    "use strict";
    // This function must not use any object literals (Object, Array, RegExp),
    // since the literals-array is being used to store the bound data.
    if (%_IsConstructCall()) {
      return %NewObjectFromBound(boundFunction);
    }
    var bindings = %BoundFunctionGetBindings(boundFunction);

    var argc = %_ArgumentsLength();
    if (argc == 0) {
      return %Apply(bindings[0], bindings[1], bindings, 2, bindings.length - 2);
    }
    if (bindings.length === 2) {
      return %Apply(bindings[0], bindings[1], arguments, 0, argc);
    }
    var bound_argc = bindings.length - 2;
    var argv = new InternalArray(bound_argc + argc);
    for (var i = 0; i < bound_argc; i++) {
      argv[i] = bindings[i + 2];
    }
    for (var j = 0; j < argc; j++) {
      argv[i++] = %_Arguments(j);
    }
    return %Apply(bindings[0], bindings[1], argv, 0, bound_argc + argc);
  };

  %FunctionRemovePrototype(boundFunction);
  var new_length = 0;
  if (%_ClassOf(this) == "Function") {
    // Function or FunctionProxy.
    var old_length = this.length;
    // FunctionProxies might provide a non-UInt32 value. If so, ignore it.
    if ((typeof old_length === "number") &&
        ((old_length >>> 0) === old_length)) {
      var argc = %_ArgumentsLength();
      if (argc > 0) argc--;  // Don't count the thisArg as parameter.
      new_length = old_length - argc;
      if (new_length < 0) new_length = 0;
    }
  }
  // This runtime function finds any remaining arguments on the stack,
  // so we don't pass the arguments object.
  var result = %FunctionBindArguments(boundFunction, this,
                                      this_arg, new_length);

  // We already have caller and arguments properties on functions,
  // which are non-configurable. It therefore makes no sence to
  // try to redefine these as defined by the spec. The spec says
  // that bind should make these throw a TypeError if get or set
  // is called and make them non-enumerable and non-configurable.
  // To be consistent with our normal functions we leave this as it is.
  // TODO(lrn): Do set these to be thrower.
  return result;

Здесь, в реализации, мы видим кучу дорогих вещей. А именно %_IsConstructCall(). Это, конечно, необходимо для соблюдения спецификации, но во многих случаях это также делает его медленнее, чем простой перенос.


С другой стороны, вызов .bindтакже немного отличается, примечания к спецификации: «Объекты функций, созданные с помощью Function.prototype.bind, не имеют свойства прототипа или внутренних [[Code]], [[FormalParameters]] и [[Scope]]» свойства "

Бенджамин Грюнбаум
источник
Если f = g.bind (stuff); должна ли f () быть медленнее, чем g (прочее)? Я могу выяснить это довольно быстро, мне просто любопытно, происходит ли одно и то же каждый раз, когда мы вызываем функцию, независимо от того, что создавало эту функцию, или это зависит от того, откуда эта функция пришла.
Пол
4
@ Пол Отнеситесь к моему ответу с некоторым скептицизмом. Все это может быть оптимизировано в будущей версии Chrome (/ V8). Я редко бывал .bindв браузере, читаемый и понятный код гораздо важнее в большинстве случаев. Что касается скорости связанных функций - да, связанные функции в данный момент будут работать медленнее , особенно когда thisзначение не используется в партиале. Вы можете увидеть это из теста производительности, из спецификации и / или из реализации независимо (тест) .
Бенджамин Грюнбаум
Интересно, если: 1) что-то изменилось с 2013 года (прошло уже два года) 2) поскольку стрелочные функции имеют эту лексическую привязку - стрелочные функции медленнее по дизайну.
Куба Wyrostek
1
@KubaWyrostek 1) Нет, 2) Нет, поскольку привязка не медленнее по дизайну, она просто не реализована так быстро. Стрелочные функции еще не появились в V8 (они появились, а затем были отменены), когда они появятся, мы увидим.
Бенджамин Грюнбаум
1
Будут ли будущие вызовы функции, к которой уже применена "привязка", работать медленнее? Т.е. a: function () {}. Bind (this) ... будут ли будущие вызовы a () медленнее, чем если бы я вообще никогда не выполнял привязку?
wayofthefuture
1

Я просто хочу показать здесь немного перспективы:

Обратите внимание, что, хотя bind()ing работает медленно, вызов функций после привязки - нет!

Мой тестовый код в Firefox 76.0 в Linux:

//Set it up.
q = function(r, s) {

};
r = {};
s = {};
a = [];
for (let n = 0; n < 1000000; ++n) {
  //Tried all 3 of these.
  //a.push(q);
  //a.push(q.bind(r));
  a.push(q.bind(r, s));
}

//Performance-testing.
s = performance.now();
for (let x of a) {
  x();
}
e = performance.now();
document.body.innerHTML = (e - s);

Таким образом, хотя это правда, что .bind()ing может быть примерно в 2 раза медленнее, чем отсутствие привязки (я тоже это тестировал), приведенный выше код занимает одинаковое количество времени для всех 3 случаев (привязка 0, 1 или 2 переменных).


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

Андрей
источник