pourquoi cette dernière fonction est-elle 10% plus rapide alors qu'elle doit créer les variables encore et encore?

14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

Et la fonction plus rapide: (notez qu'il doit toujours calculer les mêmes variables kb / mb / gb encore et encore). Où gagne-t-il des performances?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};
Tomy
la source
3
Dans tout langage de type statique, les "variables" seraient compilées sous forme de constantes. Peut-être que les moteurs JS modernes sont capables de faire la même optimisation. Cela ne semble pas fonctionner si les variables font partie d'une fermeture.
usr
6
il s'agit d'un détail d'implémentation du moteur JavaScript que vous utilisez. Le temps et l'espace théoriques sont les mêmes, ce n'est que l'implémentation d'un moteur JavaScript donné qui les fera varier. Donc, pour répondre correctement à votre question, vous devez répertorier le moteur JavaScript spécifique avec lequel vous les avez mesurés. Peut-être que quelqu'un connaît les détails de sa mise en œuvre pour dire comment / pourquoi il a rendu l'un plus optimal que l'autre. Vous devez également afficher votre code de mesure.
Jimmy Hoffa
vous utilisez le mot "calculer" en référence à des valeurs constantes; il n'y a vraiment rien à calculer dans ce à quoi vous faites référence. L'arithmétique des valeurs constantes est l'une des optimisations les plus simples et les plus évidentes des compilateurs, donc chaque fois que vous voyez une expression qui n'a que des valeurs constantes, vous pouvez simplement supposer que l'expression entière est optimisée en une seule valeur constante.
Jimmy Hoffa
@JimmyHoffa c'est vrai, mais d'un autre côté, il doit créer 3 variables constantes à chaque appel de fonction ...
Tomy
Les constantes @Tomy ne sont pas des variables. Ils ne varient pas, ils n'ont donc pas besoin d'être recréés après la compilation. Une constante est généralement placée en mémoire, et chaque portée future de cette constante est dirigée exactement au même endroit, il n'est pas nécessaire de la recréer car sa valeur ne variera jamais , donc ce n'est pas une variable. Les compilateurs n'émettent généralement pas de code qui crée des constantes, le compilateur fait la création et dirige toutes les références de code vers ce qu'il a fait.
Jimmy Hoffa

Réponses:

23

Les moteurs JavaScript modernes effectuent tous une compilation juste à temps. Vous ne pouvez pas faire de présomptions sur ce qu'il "doit créer encore et encore". Ce type de calcul est relativement facile à optimiser, dans les deux cas.

D'un autre côté, la fermeture de variables constantes n'est pas un cas typique pour lequel vous viseriez une compilation JIT. Vous créez généralement une fermeture lorsque vous souhaitez pouvoir modifier ces variables à différentes invocations. Vous créez également une déréférence de pointeur supplémentaire pour accéder à ces variables, comme la différence entre l'accès à une variable membre et un int local dans la POO.

Ce genre de situation explique pourquoi les gens abandonnent la ligne «optimisation prématurée». Les optimisations faciles sont déjà effectuées par le compilateur.

Karl Bielefeldt
la source
Je soupçonne que c'est cette traversée de portée pour une résolution variable qui cause la perte comme vous le mentionnez. Cela semble raisonnable, mais qui sait vraiment ce qu'est la folie dans un moteur JavaScript JIT ...
Jimmy Hoffa
1
Extension possible de cette réponse: la raison pour laquelle un JIT ignorerait une optimisation facile pour un compilateur hors ligne est que les performances de l'ensemble du compilateur sont plus importantes que dans des cas inhabituels.
Leushenko
12

Les variables sont bon marché. Les contextes d'exécution et les chaînes de portée sont coûteux.

Il existe différentes réponses qui se résument essentiellement à "parce que les fermetures", et celles-ci sont essentiellement vraies, mais le problème n'est pas spécifiquement avec la fermeture, c'est le fait que vous avez une fonction référençant des variables dans une portée différente. Vous auriez le même problème s'il s'agissait de variables globales sur l' windowobjet, par opposition aux variables locales à l'intérieur de l'IIFE. Essayez-le et voyez.

Donc, dans votre première fonction, lorsque le moteur voit cette déclaration:

var gbSize = size / GB;

Il doit prendre les mesures suivantes:

  1. Recherchez une variable sizedans la portée actuelle. (Je l'ai trouvé.)
  2. Recherchez une variable GBdans la portée actuelle. (Pas trouvé.)
  3. Recherchez une variable GBdans la portée parent. (Je l'ai trouvé.)
  4. Faites le calcul et assignez-le gbSize.

L'étape 3 est considérablement plus coûteuse que la simple allocation d'une variable. De plus, vous faites cela cinq fois , dont deux fois pour GBet MB. Je soupçonne que si vous les aliasiez au début de la fonction (par exemple var gb = GB) et référençiez l'alias à la place, cela produirait en fait une petite accélération, bien qu'il soit également possible que certains moteurs JS effectuent déjà cette optimisation. Et bien sûr, le moyen le plus efficace d'accélérer l'exécution est tout simplement de ne pas traverser du tout la chaîne de portée.

Gardez à l'esprit que JavaScript n'est pas comme un langage compilé de type statique où le compilateur résout ces adresses de variables au moment de la compilation. Le moteur JS doit les résoudre par leur nom , et ces recherches se produisent au moment de l'exécution, à chaque fois. Vous devez donc les éviter lorsque cela est possible.

L'affectation de variables est extrêmement bon marché en JavaScript. Ce pourrait en fait être l'opération la moins chère, bien que je n'aie rien pour étayer cette affirmation. Néanmoins, il est sûr de dire que ce n'est presque jamais une bonne idée d'essayer d' éviter de créer des variables; presque toute optimisation que vous essayez de faire dans ce domaine finira par empirer les choses, en termes de performances.

Aaronaught
la source
Et même si la « optimisation » ne, presque certainement pas d' incidence négative sur le rendement est va affecter négativement la lisibilité du code. Ce qui, à moins que vous ne fassiez des trucs de calcul fous, est le plus souvent un mauvais compromis à faire (apparemment pas d'ancre de permalien malheureusement; recherchez "2009-02-17 11:41"). Comme le résumé le dit: "Choisissez la clarté plutôt que la vitesse, si la vitesse n'est pas absolument nécessaire."
un CVn
Même lors de l'écriture d'un interpréteur très basique pour les langages dynamiques, l'accès aux variables pendant l'exécution a tendance à être une opération O (1), et la traversée de la portée O (n) n'est même pas nécessaire lors de la compilation initiale. Dans chaque portée, chaque variable nouvellement déclarée reçoit un numéro, donc étant donné que var a, b, cnous pouvons accéder en btant que scope[1]. Toutes les étendues sont numérotées, et si cette étendue est imbriquée à cinq étendues de profondeur, elle best entièrement traitée par env[5][1]ce qui est connu lors de l'analyse. Dans le code natif, les étendues correspondent aux segments de pile. Les fermetures sont plus compliquées car elles doivent sauvegarder et remplacer leenv
amon
@amon: C'est peut-être ainsi que vous aimeriez que cela fonctionne, mais ce n'est pas ainsi que cela fonctionne réellement. Des gens bien plus informés et expérimentés que moi ont écrit des livres à ce sujet; en particulier, je vous signale le JavaScript haute performance de Nicholas C. Zakas. Voici un extrait , et il a également discuté avec des références pour le sauvegarder. Bien sûr, il n'est certainement pas le seul, juste le plus connu. JavaScript a une portée lexicale, donc les fermetures ne sont vraiment pas si spéciales - essentiellement, tout est une fermeture.
Aaronaught
@Aaronaught Intéressant. Étant donné que ce livre a 5 ans, j'étais intéressé par la façon dont un moteur JS actuel gère les recherches variables et j'ai examiné le backend x64 du moteur V8. Au cours de l'analyse statique, la plupart des variables sont résolues statiquement et se voient attribuer un décalage de mémoire dans leur étendue. Les étendues de fonction sont représentées sous forme de listes liées et l'assembly est émis sous forme de boucle déroulée pour atteindre l'étendue correcte. Ici, nous obtiendrions l'équivalent du code C *(scope->outer + variable_offset)pour un accès; chaque niveau de portée de fonction supplémentaire coûte une déréférence de pointeur supplémentaire. Semble nous étions tous deux à droite :)
amon
2

Un exemple implique une fermeture, l'autre non. La mise en œuvre des fermetures est un peu délicate, car les variables fermées ne fonctionnent pas comme les variables normales. C'est plus évident dans un langage de bas niveau comme C, mais je vais utiliser JavaScript pour illustrer cela.

Une fermeture ne consiste pas seulement en une fonction, mais aussi en toutes les variables qu'elle a fermées. Lorsque nous voulons invoquer cette fonction, nous devons également fournir toutes les variables fermées. Nous pouvons modéliser une fermeture par une fonction qui reçoit un objet comme premier argument qui représente ces variables fermées:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Notez la convention d'appel maladroite que closure.apply(closure, ...realArgs)cela nécessite

La prise en charge des objets intégrés de JavaScript permet d'omettre l' varsargument explicite et nous permet d'utiliser à la thisplace:

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Ces exemples sont équivalents à ce code utilisant des fermetures:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Dans ce dernier exemple, l'objet n'est utilisé que pour regrouper les deux fonctions retournées; lethis liaison n'est pas pertinente. Tous les détails pour rendre les fermetures possibles - passer des données cachées à la fonction réelle, changer tous les accès aux variables de fermeture en recherches dans ces données cachées - sont pris en charge par le langage.

Mais appeler des fermetures implique la surcharge de la transmission de ces données supplémentaires, et l'exécution d'une fermeture implique la surcharge de recherches dans ces données supplémentaires - aggravées par une mauvaise localité de cache et généralement une déréférence de pointeur par rapport aux variables ordinaires - de sorte qu'il n'est pas surprenant que une solution qui ne repose pas sur des fermetures fonctionne mieux. D'autant plus que tout ce que votre fermeture vous permet d'économiser est quelques opérations arithmétiques extrêmement bon marché, qui pourraient même être pliées en permanence pendant l'analyse.

amon
la source