Warum ist das Binden langsamer als ein Verschluss?

79

In einem früheren Poster wurde Function.bind vs Closure in Javascript gefragt : Wie soll man wählen?

und erhielt diese Antwort teilweise, was darauf hindeutet, dass die Bindung schneller sein sollte als ein Abschluss:

Bereichsdurchquerung bedeutet, dass beim Erreichen eines Werts (Variable, Objekt), der in einem anderen Bereich vorhanden ist, zusätzlicher Overhead hinzugefügt wird (die Ausführung von Code wird langsamer).

Mit bind rufen Sie eine Funktion mit einem vorhandenen Bereich auf, sodass keine Bereichsüberquerung stattfindet.

Zwei jsperfs legen nahe, dass die Bindung tatsächlich viel, viel langsamer ist als ein Abschluss .

Dies wurde als Kommentar zu den oben genannten gepostet

Und ich beschloss, mein eigenes jsperf zu schreiben

Warum ist die Bindung so viel langsamer (70 +% auf Chrom)?

Sollte eine Bindung vermieden werden, da sie nicht schneller ist und Verschlüsse denselben Zweck erfüllen können?

Paul
quelle
10
"Sollte das Binden vermieden werden" - es sei denn, Sie tun es tausendmal auf einer Seite - sollten Sie sich nicht darum kümmern.
Zerkms
1
Das Zusammensetzen einer asynchronen komplexen Aufgabe aus kleinen Teilen erfordert möglicherweise etwas, das in nodejs genau so aussieht, da die Rückrufe irgendwie ausgerichtet werden müssen.
Paul
Ich denke, das liegt daran, dass die Browser nicht so viel Aufwand in die Optimierung gesteckt haben. Informationen zur manuellen Implementierung finden Sie im Mozilla-Code ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ). Es besteht die Möglichkeit, dass Browser dies nur intern tun, was viel mehr Arbeit als ein schnelles Schließen ist.
Dave
1
Indirekte Funktionsaufrufe ( apply/call/bind) sind im Allgemeinen viel langsamer als direkte.
Georg
@zerkms Und wer soll sagen, dass man es nicht tausendmal macht? Aufgrund der Funktionalität, die es bietet, werden Sie wahrscheinlich überrascht sein, wie häufig dies sein könnte.
Andrew

Antworten:

142

Chrome 59-Update: Wie ich in der folgenden Antwort vorausgesagt habe, ist die Bindung mit dem neuen optimierenden Compiler nicht mehr langsamer. Hier ist der Code mit Details: https://codereview.chromium.org/2916063002/

Meistens spielt es keine Rolle.

Wenn Sie keine Anwendung erstellen, in .bindder der Engpass liegt, würde ich mich nicht darum kümmern. Die Lesbarkeit ist in den meisten Fällen viel wichtiger als die reine Leistung. Ich denke, dass die Verwendung von Native .bindnormalerweise besser lesbaren und wartbaren Code bietet - was ein großes Plus ist.

Aber ja, wenn es darauf ankommt - .bindist langsamer

Ja, .bindist erheblich langsamer als ein Abschluss - zumindest in Chrome, zumindest in der aktuellen Implementierung v8. Ich persönlich musste Node.JS manchmal wegen Leistungsproblemen einschalten (im Allgemeinen sind Schließungen in leistungsintensiven Situationen etwas langsam).

Warum? Weil der .bindAlgorithmus viel komplizierter ist als das Umschließen einer Funktion mit einer anderen Funktion und das Verwenden von .calloder .apply. (Unterhaltsame Tatsache, es gibt auch eine Funktion zurück, bei der toString auf [native Funktion] gesetzt ist).

Es gibt zwei Möglichkeiten, dies unter dem Gesichtspunkt der Spezifikation und unter dem Gesichtspunkt der Implementierung zu betrachten. Beobachten wir beide.

Schauen wir uns zunächst den in der Spezifikation definierten Bindungsalgorithmus an :

  1. Sei Target dieser Wert.
  2. Wenn IsCallable (Target) false ist, lösen Sie eine TypeError-Ausnahme aus.
  3. Sei A eine neue (möglicherweise leere) interne Liste aller Argumentwerte, die nach thisArg (arg1, arg2 usw.) in der angegebenen Reihenfolge angegeben werden.

...

(21. Rufen Sie die interne Methode [[DefineOwnProperty]] von F mit den Argumenten "Argumente", PropertyDescriptor {[[Get]]: Thrower, [[Set]]: Thrower, [[Enumerable]]: false, [[Configurable] auf. ]: false} und false.

(22. Rückgabe F.

Scheint ziemlich kompliziert, viel mehr als nur ein Wrap.

Zweitens wollen wir sehen, wie es in Chrome implementiert ist .

Lassen Sie uns FunctionBindden Quellcode der Version 8 (Chrome JavaScript Engine) einchecken:

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;

Wir können hier in der Implementierung eine Reihe teurer Dinge sehen. Nämlich %_IsConstructCall(). Dies ist natürlich erforderlich, um die Spezifikation einzuhalten - aber es macht es in vielen Fällen auch langsamer als eine einfache Umhüllung.


In einem anderen Hinweis unterscheidet sich der Aufruf .bindebenfalls geringfügig. Die Spezifikationshinweise "Mit Function.prototype.bind erstellte Funktionsobjekte haben keine Prototyp-Eigenschaft oder die internen [[Code]], [[FormalParameters]] und [[Scope]] Eigenschaften"

Benjamin Gruenbaum
quelle
Wenn f = g.bind (Zeug); sollte f () langsamer sein als g (stuff)? Ich kann das ziemlich schnell herausfinden. Ich bin nur neugierig, ob jedes Mal, wenn wir eine Funktion aufrufen, dasselbe passiert, egal was diese Funktion instanziiert hat, oder ob es davon abhängt, woher diese Funktion stammt.
Paul
4
@ Paul Nimm meine Antwort mit etwas Skepsis. All dies könnte in einer zukünftigen Version von Chrome (/ V8) optimiert werden. Ich habe es selten .bindim Browser vermieden , lesbarer und verständlicher Code ist in den meisten Fällen viel wichtiger. Bezüglich der Geschwindigkeit gebundener Funktionen - Ja, gebundene Funktionen bleiben im Moment langsamer , insbesondere wenn der thisWert im Teil nicht verwendet wird. Sie sehen dies am Benchmark, an der Spezifikation und / oder an der Implementierung unabhängig (Benchmark) .
Benjamin Gruenbaum
Ich frage mich, ob: 1) sich seit 2013 etwas geändert hat (es ist jetzt zwei Jahre her) 2) seit Pfeilfunktionen diese lexikalisch gebunden haben - sind Pfeilfunktionen von Natur aus langsamer.
Kuba Wyrostek
1
@KubaWyrostek 1) Nein, 2) Nein, da das Binden von Natur aus nicht langsamer ist, wird es einfach nicht so schnell implementiert. Pfeilfunktionen sind noch nicht in V8 gelandet (sie sind gelandet und wurden dann zurückgesetzt), wenn sie sehen werden.
Benjamin Gruenbaum
1
Würden zukünftige Aufrufe einer Funktion, auf die bereits "Binden" angewendet wurde, langsamer sein? Dh a: function () {}. Bind (this) ... sind zukünftige Aufrufe von a () langsamer, als wenn ich überhaupt nicht gebunden hätte?
Wayofthefuture
1

Ich möchte hier nur ein wenig Perspektive geben:

Beachten Sie, dass während bind()ing langsam ist, ruft die Funktionen einmal gebunden ist , nicht!

Mein Testcode in Firefox 76.0 unter 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);

Während es wahr ist, dass .bind()ing etwas ~ 2X langsamer sein kann als nicht zu binden (das habe ich auch getestet), benötigt der obige Code für alle 3 Fälle die gleiche Zeit (Bindung von 0, 1 oder 2 Variablen).


Persönlich ist es mir egal, ob der .bind()Ing in meinem aktuellen Anwendungsfall langsam ist. Ich kümmere mich um die Leistung des aufgerufenen Codes, sobald diese Variablen bereits an die Funktionen gebunden sind.

Andrew
quelle