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

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

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

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

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

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

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

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

Итак, почему связывание настолько медленнее (70%% от хрома)?

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

Ответ 1

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

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

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

Однако да, когда это имеет значение - .bind медленнее

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

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

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

Сначала давайте посмотреть алгоритм привязки, определенный в спецификации:

  • Пусть Target - это значение.
  • Если IsCallable (Target) имеет значение false, введите исключение TypeError.
  • Пусть A - новый (возможно, пустой) внутренний список всех значений аргументов, предоставленных после этогоArg (arg1, arg2 и т.д.), в порядке.

...

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

(22. Возврат F.

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

Во-вторых, посмотрим как он реализован в 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 также немного отличается, примечания о спецификациях "Объекты функций, созданные с использованием функции .prototype.bind, не имеют свойства прототипа или [[Code]], [[FormalParameters]] и [[Область]] внутренние свойства"