Dlaczego oprawa jest wolniejsza niż zamknięcie?

79

Poprzedni plakat pytał Function.bind vs Closure in Javascript: jak wybrać?

i otrzymałem tę odpowiedź częściowo, która wydaje się wskazywać, że wiązanie powinno być szybsze niż zamknięcie:

Przechodzenie przez zakres oznacza, że ​​gdy dochodzi się do pobrania wartości (zmiennej, obiektu), która istnieje w innym zakresie, w związku z tym dodawany jest dodatkowy narzut (wykonanie kodu staje się wolniejsze).

Używając bind, wywołujesz funkcję z istniejącym zakresem, więc przechodzenie między zasięgiem nie ma miejsca.

Dwa jsperf sugerują, że bind jest w rzeczywistości dużo, dużo wolniejszy niż zamknięcie .

Zostało to opublikowane jako komentarz do powyższego

Postanowiłem napisać własny plik jsperf

Dlaczego więc wiąże się o wiele wolniej (70 +% na chromie)?

Skoro nie jest to szybsze, a zamknięcia mogą służyć temu samemu celowi, czy należy unikać wiązania?

Paweł
źródło
10
„Należy unikać wiązania” - chyba że robisz to tysiące razy na jednej stronie - nie powinieneś się tym przejmować.
zerkms
1
Składanie asynchronicznego złożonego zadania z małych kawałków może wymagać czegoś, co wygląda dokładnie tak, w nodejs, ponieważ wywołania zwrotne muszą być w jakiś sposób wyrównane.
Paul,
Myślę, że dzieje się tak dlatego, że przeglądarki nie włożyły tyle wysiłku w jego optymalizację. Zobacz kod Mozilli ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ), aby zaimplementować go ręcznie. Jest duża szansa, że ​​przeglądarki robią to wewnętrznie, co wymaga znacznie więcej pracy niż szybkie zamknięcie.
Dave,
1
Pośrednie wywołania funkcji ( apply/call/bind) są generalnie znacznie wolniejsze niż bezpośrednie.
georg
@zerkms A kto powie, że nie robi się tego tysiące razy? Ze względu na funkcjonalność, którą zapewnia, myślę, że możesz być zaskoczony, jak często może to być.
Andrew

Odpowiedzi:

142

Aktualizacja Chrome 59: Jak przewidziałem w odpowiedzi poniżej, bind nie jest już wolniejszy z nowym optymalizującym kompilatorem. Oto kod ze szczegółami: https://codereview.chromium.org/2916063002/

W większości przypadków nie ma to znaczenia.

Chyba że tworzysz aplikację, w której .bindjest wąskie gardło, nie zawracałbym sobie głowy. W większości przypadków czytelność jest znacznie ważniejsza niż sama wydajność. Myślę, że użycie natywnego .bindzwykle zapewnia bardziej czytelny i łatwiejszy w utrzymaniu kod - co jest dużym plusem.

Jednak tak, kiedy ma to znaczenie - .bindjest wolniejsze

Tak, .bindjest znacznie wolniejsze niż zamknięcie - przynajmniej w Chrome, przynajmniej w obecnym sposobie implementacji v8. Osobiście musiałem czasami przełączać się w Node.JS z powodu problemów z wydajnością (ogólnie rzecz biorąc, zamknięcia są trochę powolne w sytuacjach wymagających dużej wydajności).

Czemu? Ponieważ .bindalgorytm jest o wiele bardziej skomplikowany niż zawijanie funkcji inną funkcją i użycie .calllub .apply. (Ciekawostka, zwraca także funkcję z parametrem toString ustawionym na [funkcja natywna]).

Można na to spojrzeć na dwa sposoby, z punktu widzenia specyfikacji oraz z punktu widzenia implementacji. Przyjrzyjmy się obu.

Najpierw przyjrzyjmy się algorytmowi wiązania zdefiniowanemu w specyfikacji :

  1. Niech Target będzie tą wartością.
  2. Jeśli IsCallable (Target) ma wartość false, zgłoś wyjątek TypeError.
  3. Niech A będzie nową (prawdopodobnie pustą) wewnętrzną listą wszystkich wartości argumentów podanych po thisArg (arg1, arg2 itd.), W kolejności.

...

(21. Wywołaj wewnętrzną metodę [[DefineOwnProperty]] F z argumentami „arguments”, PropertyDescriptor {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable] ]: false} i false.

(22. Powrót F.

Wydaje się dość skomplikowane, znacznie więcej niż tylko chusta.

Po drugie, zobaczmy, jak jest zaimplementowany w Chrome .

Sprawdźmy w FunctionBindkodzie źródłowym v8 (silnik Chrome JavaScript):

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;

W implementacji widzimy wiele drogich rzeczy. Mianowicie %_IsConstructCall(). Jest to oczywiście konieczne, aby zachować zgodność ze specyfikacją - ale w wielu przypadkach powoduje również, że jest wolniejsze niż zwykłe zawijanie.


Z drugiej strony, wywołanie .bindjest również nieco inne, uwagi specyfikacji „Obiekty funkcji utworzone za pomocą funkcji Function.prototype.bind nie mają właściwości prototypu ani wewnętrznych właściwości [[Code]], [[FormalParameters]] i [[Scope]] nieruchomości"

Benjamin Gruenbaum
źródło
Jeśli f = g.bind (rzeczy); czy f () powinno być wolniejsze niż g (rzeczy)? Mogę się tego dowiedzieć dość szybko, jestem po prostu ciekawy, czy to samo dzieje się za każdym razem, gdy wywołujemy funkcję, bez względu na to, która instancja tej funkcji została utworzona, czy też zależy od tego, skąd ta funkcja pochodzi.
Paul,
4
@Paul Przyjmij moją odpowiedź z pewnym sceptycyzmem. Wszystko to może zostać zoptymalizowane w przyszłej wersji Chrome (/ V8). Rzadko kiedy unikałem .bindw przeglądarce, czytelny i zrozumiały kod jest o wiele ważniejszy w większości przypadków. Jeśli chodzi o szybkość funkcji związanych - tak, funkcje ograniczone pozostaną w tej chwili wolniejsze , zwłaszcza gdy thiswartość nie jest używana w częściowej. Możesz to zobaczyć na podstawie testu porównawczego, specyfikacji i / lub niezależnie od wdrożenia (benchmark) .
Benjamin Gruenbaum,
Zastanawiam się, czy: 1) coś się zmieniło od 2013 roku (minęły już dwa lata), 2) ponieważ funkcje strzałkowe mają to leksykalnie ograniczone - czy funkcje strzałkowe są z założenia wolniejsze.
Kuba Wyrostek
1
@KubaWyrostek 1) Nie, 2) Nie, ponieważ bind nie jest wolniejszy z założenia, po prostu nie jest wdrażany tak szybko. Funkcje strzałek nie wylądowały jeszcze w V8 (wylądowały, a następnie zostały przywrócone), kiedy się pojawią, zobaczymy.
Benjamin Gruenbaum
1
Czy przyszłe wywołania funkcji, do której zastosowano już „bind”, byłyby wolniejsze? To znaczy: function () {}. Bind (this) ... czy przyszłe wywołania a () są wolniejsze, niż gdybym nigdy nie był związany?
wayofthefuture
1

Chcę tu tylko trochę spojrzeć:

Zauważ, że podczas gdy bind()ing jest powolne, wywoływanie funkcji raz związanych już nie!

Mój kod testowy w przeglądarce Firefox 76.0 w systemie 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);

Więc chociaż prawdą jest, że .bind()ing może być około ~ 2X wolniejsze niż niewiążące (testowałem to również), powyższy kod zajmuje taką samą ilość czasu dla wszystkich 3 przypadków (wiązanie 0, 1 lub 2 zmiennych).


Osobiście nie obchodzi mnie, czy .bind()ingowanie jest powolne w moim obecnym przypadku użycia, zależy mi na wydajności wywoływanego kodu, gdy te zmienne są już powiązane z funkcjami.

Andrzej
źródło