Чому зв'язування відбувається повільніше, ніж закриття?


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, встановлену на [рідна функція]).

Існує два способи розглянути це, з точки зору специфікації та з точки зору реалізації. Давайте спостерігатимемо обидва.

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

  1. Нехай Target - це значення.
  2. Якщо IsCallable (Target) хибне, викиньте виняток TypeError.
  3. Нехай A - це новий (можливо, порожній) внутрішній список усіх значень аргументів, наданих після thisArg (arg1, arg2 тощо), по порядку.

...

(21. Викличте [[DefineOwnProperty]] внутрішній метод F з аргументами «аргументи», 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, не мають властивості прототипу або внутрішніх [[Код]], [[ФормальніПараметри]] та [[Сфера]] властивості "


Якщо f = g.bind (stuff); чи f () повинен бути повільнішим за g (речі)? Я можу це досить швидко дізнатись, мені просто цікаво, чи те саме відбувається кожного разу, коли ми викликаємо функцію, незалежно від того, який екземпляр цієї функції, або якщо це залежить, звідки ця функція походить.
Пол

4
@ Пол, сприйміть мою відповідь з деяким скептицизмом. Все це може бути оптимізовано в майбутній версії Chrome (/ V8). Я рідко виявляв, що уникаю цього .bindв браузері, читабельний і зрозумілий код набагато важливіший у більшості випадків. Що стосується швидкості зв’язаних функцій - Так, зв’язані функції в даний момент залишаться повільнішими , особливо коли thisзначення не використовується в частковому. Ви можете переконатися в цьому з тесту, із специфікації та / або з реалізації самостійно (бенчмарк) .
Бенджамін Груенбаум

Цікаво, якщо: 1) що-небудь змінилося з 2013 року (минуло вже два роки) 2), оскільки функції стрілок мають це лексично зв’язок - це функції стрілок повільніші за дизайном.
Kuba Wyrostek

1
@KubaWyrostek 1) Ні, 2) Ні, оскільки прив'язка не є повільнішою за дизайном, вона просто реалізується не так швидко. Функції стрілок ще не потрапили у V8 (вони приземлилися, а потім були повернуті), коли вони побачать.
Бенджамін Груенбаум,

1
Чи будуть майбутні виклики функції, до якої вже застосовано функцію "прив'язки", повільнішими? Тобто a: function () {}. Bind (this) ... чи є майбутні виклики a () дещо повільнішими, ніж якщо б я ніколи не прив'язував спочатку?
шлях майбутнього

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()в моєму теперішньому випадку використання інґ повільний, я дбаю про продуктивність коду, який викликається, коли ці змінні вже прив’язані до функцій.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.