76 votes

Pourquoi le bind est-il plus lent que la fermeture ?

Une personne a posé la question suivante Function.bind vs Closure en Javascript : comment choisir ?

et j'ai reçu cette réponse en partie, qui semble indiquer que le liage devrait être plus rapide qu'une fermeture :

S (variable, objet) qui existe dans un autre champ d'application. un surcoût supplémentaire est ajouté (le code devient plus lent à exécuter).

U la traversée du champ d'application n'a pas lieu.

Deux jsperfs suggèrent que bind est en fait beaucoup, beaucoup plus lent qu'un fermeture .

Ceci a été posté en tant que commentaire à l'article ci-dessus

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

Alors pourquoi bind est-il si lent (70+% sur chrome) ?

Étant donné qu'elle n'est pas plus rapide et que les fermetures peuvent servir le même objectif, la liaison doit-elle être évitée ?

137voto

Benjamin Gruenbaum Points 51406

Mise à jour Chrome 59 : Comme je l'avais prédit dans la réponse ci-dessous, bind n'est plus lent avec le nouveau compilateur optimisant. Voici le code avec les détails : https://codereview.chromium.org/2916063002/

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

À moins que vous ne créiez une application dans laquelle .bind est le goulot d'étranglement, je ne m'en préoccuperais pas. La lisibilité est bien plus importante que la performance pure dans la plupart des cas. Je pense que l'utilisation de .bind permet généralement d'obtenir un code plus lisible et plus facile à maintenir, ce qui est un grand avantage.

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

Oui, .bind est considérablement plus lent qu'une fermeture - du moins dans Chrome, du moins dans la façon dont il est actuellement implémenté dans v8 . J'ai personnellement dû passer à Node.JS pour des questions de performance (plus généralement, les fermetures sont un peu lentes dans les situations où les performances sont élevées).

Pourquoi ? Parce que les .bind est beaucoup plus compliqué que d'envelopper une fonction dans une autre fonction et d'utiliser l'algorithme .call ou .apply . (En fait, elle renvoie également une fonction dont la valeur de toString est [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 la mise en œuvre. Observons les deux.

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

  1. Soit Cible cette valeur.
  2. Si IsCallable(Target) est faux, une exception TypeError est levée.
  3. Soit A une nouvelle liste interne (éventuellement vide) de toutes les valeurs d'argument fournies après cetArg (arg1, arg2, etc.), dans l'ordre.

...

(21. appeler la méthode interne [[DefineOwnProperty]] de F avec les arguments "arguments", PropertyDescriptor {[[Get]] : lanceur, [[Set]] : lanceur, [[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 mis en œuvre dans Chrome .

Vérifions FunctionBind dans le code source de la 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;

Nous pouvons voir un certain nombre de choses coûteuses ici dans la mise en œuvre. A savoir %_IsConstructCall() . Cela est bien sûr nécessaire pour respecter la spécification, mais cela rend le système plus lent qu'une simple enveloppe dans de nombreux cas.


Par ailleurs, le fait d'appeler .bind est également légèrement différent, la spécification indique que "les objets Function créés à l'aide de Function.prototype.bind n'ont pas de propriété prototype ni les propriétés internes [[Code]], [[FormalParameters]] et [[Scope]]"

6voto

Andrew Points 959

Je voudrais juste donner un peu de perspective à ce sujet :

Il convient de noter que si les bind() ingurgiter est lent, appel les fonctions une fois liées ne le sont pas !

Mon code de test dans Firefox 76.0 sous 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);

Ainsi, s'il est vrai que .bind() Le code ci-dessus prend le même temps dans les trois cas (0, 1 ou 2 variables).


Personnellement, je me fiche de savoir si le .bind() est lent dans mon cas d'utilisation actuel, je m'intéresse à la performance du code appelé une fois que ces variables sont déjà liées aux fonctions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X