web-dev-qa-db-fra.com

Pourquoi la liaison est-elle plus lente qu'une fermeture?

Une affiche précédente demandait Function.bind vs Closure in Javascript: comment choisir?

et a reçu cette réponse en partie, ce qui semble indiquer que la liaison devrait être plus rapide qu'une fermeture:

La traversée de la portée signifie que lorsque vous atteignez une valeur (variable, objet) qui existe dans une portée différente, une surcharge supplémentaire est ajoutée (le code devient plus lent à exécuter).

En utilisant bind, vous appelez une fonction avec une portée existante, de sorte que la traversée de la portée n'a pas lieu.

Deux jsperfs suggèrent que la liaison est en réalité beaucoup, beaucoup plus lente qu'un fermeture .

Cela a été publié en tant que commentaire de ce qui précède

Et j'ai décidé d'écrire mon propre jsperf

Alors pourquoi la liaison est-elle tellement plus lente (70 +% sur le chrome)?

Puisqu'il n'est pas plus rapide et que les fermetures peuvent servir le même objectif, faut-il éviter les liens?

75
Paul

Mise à jour de Chrome 59: comme je l'avais prédit dans la réponse ci-dessous, la liaison n'est plus plus lente avec le nouveau compilateur d'optimisation. Voici le code avec des détails: https://codereview.chromium.org/2916063002/

La plupart du temps, cela n'a pas d'importance.

Sauf si vous créez une application où .bind Est le goulot d'étranglement que je ne dérangerais pas. La lisibilité est beaucoup plus importante que la simple performance dans la plupart des cas. Je pense que l'utilisation de .bind Natif fournit généralement un code plus lisible et plus facile à gérer - ce qui est un gros plus.

Mais oui, quand c'est important - .bind Est plus lent

Oui, .bind Est considérablement plus lent qu'une fermeture - au moins dans Chrome, au moins de la manière actuelle, il est implémenté dans v8. J'ai personnellement dû basculer dans Node.JS pour des problèmes de performances à certains moments (plus généralement, les fermetures sont un peu lentes dans des situations exigeantes en performances).

Pourquoi? Parce que l'algorithme .bind Est beaucoup plus compliqué que d'envelopper une fonction avec une autre fonction et d'utiliser .call Ou .apply. (Fait amusant, il renvoie également une fonction avec toString définie sur [fonction native]).

Il y a deux façons de voir les choses, du point de vue de la spécification et du point de vue de l'implémentation. Observons les deux.

Tout d'abord, allons regardez l'algorithme de liaison défini dans la spécification :

  1. Soit Target la valeur this.
  2. Si IsCallable (Target) est false, lève une exception TypeError.
  3. Soit A une nouvelle liste interne (éventuellement vide) de toutes les valeurs d'argument fournies après thisArg (arg1, arg2 etc.), dans l'ordre.

...

(21. Appelez la méthode interne [[DefineOwnProperty]] de F avec des arguments "arguments", PropertyDescriptor {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable] ]: false} et false.

(22. Retour F.

Cela semble assez compliqué, bien plus qu'un simple emballage.

Deuxièmement, voyons comment il est implémenté dans Chrome .

Vérifions FunctionBind dans le code source de la v8 (moteur JavaScript Chrome):

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;

Nous pouvons voir un tas de choses coûteuses ici dans la mise en œuvre. À savoir %_IsConstructCall(). Bien sûr, cela est nécessaire pour respecter les spécifications - mais cela le rend également plus lent qu'un simple emballage dans de nombreux cas.


Sur une autre note, l'appel de .bind Est également légèrement différent, les notes de spécification "Les objets Function créés à l'aide de Function.prototype.bind n'ont pas de propriété prototype ou les [[Code]], [[FormalParameters]], et [[Scope]] internal properties "

135
Benjamin Gruenbaum