v8 Implications des performances JavaScript de const, let et var?

88

Indépendamment des différences fonctionnelles, l'utilisation des nouveaux mots-clés «let» et «const» a-t-elle un impact généralisé ou spécifique sur les performances par rapport à «var»?

Après avoir exécuté le programme:

function timeit(f, N, S) {
    var start, timeTaken;
    var stats = {min: 1e50, max: 0, N: 0, sum: 0, sqsum: 0};
    var i;
    for (i = 0; i < S; ++i) {
        start = Date.now();
        f(N);
        timeTaken = Date.now() - start;

        stats.min = Math.min(timeTaken, stats.min);
        stats.max = Math.max(timeTaken, stats.max);
        stats.sum += timeTaken;
        stats.sqsum += timeTaken * timeTaken;
        stats.N++
    }

    var mean = stats.sum / stats.N;
    var sqmean = stats.sqsum / stats.N;

    return {min: stats.min, max: stats.max, mean: mean, spread: Math.sqrt(sqmean - mean * mean)};
}

var variable1 = 10;
var variable2 = 10;
var variable3 = 10;
var variable4 = 10;
var variable5 = 10;
var variable6 = 10;
var variable7 = 10;
var variable8 = 10;
var variable9 = 10;
var variable10 = 10;

function varAccess(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += variable1;
        sum += variable2;
        sum += variable3;
        sum += variable4;
        sum += variable5;
        sum += variable6;
        sum += variable7;
        sum += variable8;
        sum += variable9;
        sum += variable10;
    }
    return sum;
}

const constant1 = 10;
const constant2 = 10;
const constant3 = 10;
const constant4 = 10;
const constant5 = 10;
const constant6 = 10;
const constant7 = 10;
const constant8 = 10;
const constant9 = 10;
const constant10 = 10;

function constAccess(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += constant1;
        sum += constant2;
        sum += constant3;
        sum += constant4;
        sum += constant5;
        sum += constant6;
        sum += constant7;
        sum += constant8;
        sum += constant9;
        sum += constant10;
    }
    return sum;
}


function control(N) {
    var i, sum;
    for (i = 0; i < N; ++i) {
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
        sum += 10;
    }
    return sum;
}

console.log("ctl = " + JSON.stringify(timeit(control, 10000000, 50)));
console.log("con = " + JSON.stringify(timeit(constAccess, 10000000, 50)));
console.log("var = " + JSON.stringify(timeit(varAccess, 10000000, 50)));

.. Mes résultats sont les suivants:

ctl = {"min":101,"max":117,"mean":108.34,"spread":4.145407097016924}
con = {"min":107,"max":572,"mean":435.7,"spread":169.4998820058587}
var = {"min":103,"max":608,"mean":439.82,"spread":176.44417700791374}

Cependant, la discussion comme indiqué ici semble indiquer un réel potentiel de différences de performance dans certains scénarios: https://esdiscuss.org/topic/performance-concern-with-let-const

sean2078
la source
Je pense que cela dépend de l'utilisation, par exemple, letutilisé dans la portée de bloc devrait être plus performant que var, qui n'a pas de portée de bloc, mais seulement une portée de fonction.
adeneo
Si je peux demander, pourquoi est-ce @adeneo?
sean2078
1
@ sean2078 - si vous avez besoin de déclarer une variable qui ne vit que dans une portée de bloc, le letferait, puis le ramasse-miettes, tandis que var, qui est à portée de fonction, ne fonctionnerait pas nécessairement de la même manière. Encore une fois, je pense que c'est tellement spécifique à l'utilisation, que les deux letet const peuvent être plus performants, mais ne le seront pas toujours.
adeneo
1
Je ne sais pas comment le code cité est censé démontrer toute différence entre varet let: il n'utilise jamais letdu tout.
TJ Crowder
1
Actuellement, ce n'est pas le cas - seulement const vs var .. Provenant à l'origine de gist.github.com/srikumarks/1431640 (crédit à srikumarks) mais une demande a été faite pour remettre le code en question
sean2078

Réponses:

116

TL; DR

En théorie , une version non optimisée de cette boucle:

for (let i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

peut être plus lent qu'une version non optimisée de la même boucle avec var:

for (var i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

car une variable différente i est créée pour chaque itération de boucle avec let, alors qu'il n'y en a qu'une seule iavec var.

Contre cela, c'est le fait que le varest hissé, donc il est déclaré en dehors de la boucle alors que le letn'est déclaré que dans la boucle, ce qui peut offrir un avantage d'optimisation.

En pratique , ici en 2018, les moteurs JavaScript modernes font suffisamment d'introspection de la boucle pour savoir quand ils peuvent optimiser cette différence. (Même avant, il y a de fortes chances que votre boucle fasse suffisamment de travail pour que les letfrais généraux supplémentaires soient supprimés de toute façon. Mais maintenant, vous n'avez même plus à vous en soucier.)

Méfiez-vous des benchmarks synthétiques car ils sont extrêmement faciles à se tromper et déclenchent les optimiseurs de moteur JavaScript d'une manière que le vrai code ne fait pas (à la fois bonne et mauvaise). Cependant, si vous voulez un benchmark synthétique, en voici un:

Il dit qu'il n'y a pas de différence significative dans ce test synthétique sur V8 / Chrome ou SpiderMonkey / Firefox. (Les tests répétés dans les deux navigateurs ont l'un gagnant, ou l'autre gagnant, et dans les deux cas dans une marge d'erreur.) Mais encore une fois, c'est une référence synthétique, pas votre code. Souciez-vous des performances de votre code quand et si votre code a un problème de performances.

En ce qui concerne le style, je préfère letpour l'avantage de la portée et l'avantage de la fermeture dans les boucles si j'utilise la variable de boucle dans une fermeture.

Détails

La différence importante entre varet letdans une forboucle est qu'un différent iest créé pour chaque itération; il résout le problème classique des "fermetures en boucle":

Créer le nouvel EnvironmentRecord pour chaque corps de boucle ( lien de spécification ) est un travail, et le travail prend du temps, c'est pourquoi en théorie la letversion est plus lente que la varversion.

Mais la différence n'a d'importance que si vous créez une fonction (fermeture) dans la boucle qui utilise i, comme je l'ai fait dans cet exemple d'extrait de code exécutable ci-dessus. Sinon, la distinction ne peut pas être observée et peut être optimisée.

Ici en 2018, il semble que V8 (et SpiderMonkey dans Firefox) fasse une introspection suffisante pour qu'il n'y ait aucun coût de performance dans une boucle qui n'utilise pas la letsémantique de variable par itération de. Voir ce test .


Dans certains cas, cela constpourrait bien offrir une opportunité d'optimisation qui varne le serait pas, en particulier pour les variables globales.

Le problème avec une variable globale est qu'elle est, eh bien, globale; n'importe quel code n'importe où pourrait y accéder. Donc, si vous déclarez une variable avec varlaquelle vous n'avez jamais l'intention de changer (et ne changez jamais votre code), le moteur ne peut pas supposer qu'elle ne changera jamais à la suite d'un code chargé plus tard ou similaire.

Avec const, cependant, vous dites explicitement au moteur que la valeur ne peut pas changer¹. Il est donc libre de faire toute optimisation qu'il souhaite, y compris d'émettre un littéral au lieu d'une référence de variable au code qui l'utilise, sachant que les valeurs ne peuvent pas être modifiées.

¹ N'oubliez pas qu'avec les objets, la valeur est une référence à l'objet, pas à l'objet lui-même. Donc avec const o = {}, vous pouvez changer l'état de l'objet ( o.answer = 42), mais vous ne pouvez pas opointer vers un nouvel objet (car cela nécessiterait de changer la référence d'objet qu'il contient).


Lors de l'utilisation letou constdans d'autres varsituations similaires, il est peu probable qu'ils aient des performances différentes. Cette fonction doit avoir exactement les mêmes performances que vous utilisiez varou let, par exemple:

function foo() {
    var i = 0;
    while (Math.random() < 0.5) {
        ++i;
    }
    return i;
}

Tout cela, bien sûr, n'a pas d'importance et quelque chose à craindre uniquement si et quand il y a un vrai problème à résoudre.

TJ Crowder
la source
Merci pour la réponse - je suis d'accord, donc pour moi-même, j'ai normalisé l'utilisation de var pour les opérations de boucle comme indiqué dans votre premier exemple de boucle for, et let / const pour toutes les autres déclarations en supposant que la différence de performance est essentiellement inexistante comme le test de performance semble à indiquer pour l'instant. Peut-être plus tard, des optimisations sur const seront ajoutées. Autrement dit, à moins que quelqu'un d'autre puisse montrer une différence perceptible via un exemple de code.
sean2078
@ sean2078: J'utilise également letdans l'exemple de boucle. La différence de performances ne vaut tout simplement pas la peine de s'en inquiéter dans le cas de 99,999%.
TJ Crowder
2
À partir de la mi-2018, les versions avec let et var ont la même vitesse dans Chrome, il n'y a donc plus de différence.
Max
1
@DanM: Bonne nouvelle, l'optimisation semble avoir rattrapé son retard, du moins dans le V8 et SpiderMonkey. :-)
TJ Crowder
1
Merci. C'est suffisant.
hypers
18

"LET" EST MIEUX DANS LES DÉCLARATIONS DE BOUCLE

Avec un simple test (5 fois) dans le navigateur comme ça:

// WITH VAR
console.time("var-time")
for(var i = 0; i < 500000; i++){}
console.timeEnd("var-time")

Le temps moyen d'exécution est supérieur à 2,5 ms

// WITH LET
console.time("let-time")
for(let i = 0; i < 500000; i++){}
console.timeEnd("let-time")

Le temps moyen d'exécution est supérieur à 1,5 ms

J'ai trouvé que le temps de boucle avec let est meilleur.

Amn
la source
6
En exécutant ceci dans Firefox 65.0, j'ai des vitesses moyennes de var=138.8mset let=4ms. Ce n'est pas une faute de frappe, letc'est plus de 30 fois plus rapide en ce moment
Katamari
6
Je viens de tester cela dans Node v12.5. J'ai trouvé que les vitesses moyennes sont var=2.6mset let=1.0ms. Alors laissez entrer Node est un peu plus de deux fois plus rapide.
Kane Hooper
2
Juste pour faire valoir que les tests de performances sont difficiles en présence d'optimiseurs: je pense que la boucle let est entièrement optimisée - let n'existe qu'à l'intérieur du bloc et la boucle n'a aucun effet secondaire et V8 est assez intelligent pour savoir qu'il peut retirez simplement le bloc, puis la boucle. la déclaration var est hissée donc elle ne peut pas le savoir. Vos boucles telles qu'elles sont j'obtiens 1ms / 0.4ms, cependant si pour les deux j'ai une variable j (var ou let) en dehors de la boucle qui est également incrémentée, j'obtiens alors 1ms / 1.5ms. ie var boucle pas de changement, laissez la boucle prend plus de temps.
Euan Smith le
@KaneHooper - Si vous avez une différence quintuple dans Firefox, il faudra que ce soit le corps de la boucle vide qui l'a fait. Les vraies boucles n'ont pas de corps vides.
TJ Crowder
Attention aux benchmarks synthétiques , et en particulier à ceux avec des boucles avec des corps vides. Si vous faites réellement quelque chose dans la boucle, ce benchmark synthétique (qui, encore une fois, méfiez-vous! :-)) suggère qu'il n'y a pas de différence significative. J'en ai aussi ajouté un à ma réponse donc c'est sur place (pas comme ces tests jsPerf qui ne cessaient de disparaître sur moi. :-)). Les courses répétées montrent que l'un est gagnant ou l'autre gagnant. Certainement rien de concluant.
TJ Crowder
8

La réponse de TJ Crowder est tellement excellente.

Voici un ajout de: "Quand en aurais-je le plus pour mon argent en éditant les déclarations var existantes dans const?"

J'ai trouvé que la plus grande amélioration des performances était liée aux fonctions "exportées".

Donc, si les fichiers A, B, R et Z font appel à une fonction "utilitaire" dans le fichier U qui est couramment utilisée via votre application, alors basculer cette fonction utilitaire sur "const" et la référence du fichier parent à un const peut eak des performances améliorées. Il me semblait que ce n'était pas beaucoup plus rapide, mais la consommation globale de mémoire a été réduite d'environ 1 à 3% pour mon application Frankenstein-ed grossièrement monolithique. Ce qui, si vous dépensez des sacs d'argent sur le cloud ou votre serveur baremetal, pourrait être une bonne raison de passer 30 minutes à parcourir et mettre à jour certaines de ces déclarations var vers const.

Je me rends compte que si vous lisez comment const, var, et laissez travailler sous les couvertures, vous avez probablement déjà conclu ce qui précède ... mais au cas où vous auriez "jeté un coup d'œil" dessus: D.

D'après ce que je me souviens de l'analyse comparative sur le nœud v8.12.0 lorsque je faisais la mise à jour, mon application est passée d'une consommation inactive d'environ 240 Mo de RAM à ~ 233 Mo de RAM.

isaacdre
la source
2

La réponse de TJ Crowder est très bonne mais:

  1. 'let' est fait pour rendre le code plus lisible, pas plus puissant
  2. par théorie let sera plus lent que var
  3. par la pratique le compilateur ne peut pas résoudre complètement (analyse statique) un programme incomplet donc parfois il manquera l'optimisation
  4. dans tous les cas, l'utilisation de 'let' nécessitera plus de CPU pour l'introspection, le banc doit être démarré lorsque google v8 commence à analyser
  5. si l'introspection échoue, «let» poussera fort sur le ramasse-miettes V8, il faudra plus d'itérations pour libérer / réutiliser. il consommera également plus de RAM. le banc doit tenir compte de ces points
  6. Google Closure transformera let in var ...

L'effet de l'écart de performance entre var et let peut être vu dans un programme complet réel et non sur une seule boucle de base.

Quoi qu'il en soit, utiliser let là où vous n'êtes pas obligé de rendre votre code moins lisible.

Michael Valve
la source