Dépendance des performances du code sur le contexte de la déclaration des variables en JavaScript


Initialement, cet article a été conçu comme une petite référence pour son propre usage, et en général il n'était pas prévu d'être un article, cependant, dans le processus de prise de mesures, certaines fonctionnalités intéressantes sont apparues dans la mise en œuvre de l'architecture JavaScript qui affectent fortement les performances du code final dans certains cas. Je suggère, et vous, de vous familiariser avec les résultats obtenus, en passant également en revue certains sujets connexes: pour les boucles, l'environnement (contexte d'exécution) et les blocs.

À la fin de mon article «Utilisation des déclarations de variables let et des fonctionnalités des fermetures JavaScript résultantes», j'ai brièvement abordé la comparaison des performances des déclarations de variables let (LexicalDeclaration) et var (VarDeclaredNames) en boucles. À titre de comparaison, nous avons utilisé le temps d'exécution du tri manuel (sans l'aide de Array.prototype.sort () ) du tableau, l'une des méthodes les plus simples est le tri par sélection, car avec une longueur de tableau de 100000, nous avons obtenu un peu plus de 5 milliards. itérations en deux cycles (externe et imbriqué), et, ce montant devrait permettre au final une estimation adéquate.

Pour var, il triait la vue:

for (var i = 0, len = arr.length; i < len-1; i++) { var min, mini = i; for (var j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 9.082 . //   Chrome: 10.783 . 

Et pour laisser :

 for (let i = 0, len = arr.length; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.261 . //   Chrome: 5.391 . 

En voyant ces chiffres, il semblerait, on peut affirmer sans équivoque que les publicités dépassent complètement la vitesse de variation. Mais, en plus de cette conclusion, la question est restée en suspens: que se passera-t-il si nous mettons les déclarations let en dehors des boucles for ?

Mais, avant de faire cela, vous devez approfondir le travail de la boucle for , guidé par la spécification actuelle d' ECMAScript 2019 (ECMA-262) :

 13.7.4.7Runtime Semantics: LabelledEvaluation With parameter labelSet. IterationStatement':'for(Expression;Expression;Expression)Statement 1. If the first Expression is present, then a. Let exprRef be the result of evaluating the first Expression. b. Perform ? GetValue(exprRef). 2. Return ? ForBodyEvaluation(the second Expression, the third Expression, Statement, « », labelSet). IterationStatement':'for(varVariableDeclarationList;Expression;Expression)Statement 1. Let varDcl be the result of evaluating VariableDeclarationList. 2. ReturnIfAbrupt(varDcl). 3. Return ? ForBodyEvaluation(the first Expression, the second Expression, Statement, « », labelSet). IterationStatement':'for(LexicalDeclarationExpression;Expression)Statement 1. Let oldEnv be the running execution context's LexicalEnvironment. 2. Let loopEnv be NewDeclarativeEnvironment(oldEnv). 3. Let loopEnvRec be loopEnv's EnvironmentRecord. 4. Let isConst be the result of performing IsConstantDeclaration of LexicalDeclaration. 5. Let boundNames be the BoundNames of LexicalDeclaration. 6. For each element dn of boundNames, do a. If isConst is true, then i. Perform ! loopEnvRec.CreateImmutableBinding(dn, true). b. Else, i. Perform ! loopEnvRec.CreateMutableBinding(dn, false). 7. Set the running execution context's LexicalEnvironment to loopEnv. 8. Let forDcl be the result of evaluating LexicalDeclaration. 9. If forDcl is an abrupt completion, then a. Set the running execution context's LexicalEnvironment to oldEnv. b. Return Completion(forDcl). 10. If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be « ». 11. Let bodyResult be ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet). 12. Set the running execution context's LexicalEnvironment to oldEnv. 13. Return Completion(bodyResult). 
Remarque: les deux-points après IterationStatements, dans la source ne sont pas encadrés par des apostrophes - sont ajoutés ici afin qu'il n'y ait pas de formatage automatique qui gâche à peu près la lisibilité du texte.

Ici, comme nous le voyons, il y a trois options pour appeler et poursuivre le travail de la boucle for :
  • avec instruction for (Expression; Expression; Expression)
    ForBodyEvaluation (la deuxième expression, la troisième expression, instruction, "", labelSet) .
  • avec l' instruction for (varVariableDeclarationList; Expression; Expression)
    ForBodyEvaluation (la première expression, la deuxième expression, instruction, "", labelSet).
  • at for (LexicalDeclarationExpression; Expression), instruction
    ForBodyEvaluation (la première expression, la deuxième expression, instruction, perIterationLets, labelSet)

Dans la dernière, troisième variante, contrairement aux deux premières, le quatrième paramètre n'est pas vide - perIterationLets - ce sont en fait les mêmes déclarations let dans le premier paramètre passé à la boucle for . Ils sont spécifiés au paragraphe 10:
- Si isConst est faux , laissez perIterationLets être boundNames; sinon laissez perIterationLets être "".
Si une constante a été passée à for , mais pas une variable, le paramètre perIterationLets devient vide.

De plus, dans la troisième option, il est nécessaire de prêter attention au paragraphe 2:
- Laissez loopEnv être NewDeclarativeEnvironment (oldEnv).

 8.1.2.2NewDeclarativeEnvironment ( E ) When the abstract operation NewDeclarativeEnvironment is called with a Lexical Environment as argument E the following steps are performed: 1. Let env be a new Lexical Environment. 2. Let envRec be a new declarative Environment Record containing no bindings. 3. Set env's EnvironmentRecord to envRec. 4. Set the outer lexical environment reference of env to E. 5. Return env. 

Ici, en tant que paramètre E , l'environnement à partir duquel la boucle for a été appelée (global, n'importe quelle fonction, etc.) est pris, et un nouvel environnement est créé pour exécuter la boucle for en référence à l'environnement externe qui l'a créée (point 4). Nous sommes intéressés par ce fait car l'environnement est un contexte d'exécution.

Et nous nous souvenons que les déclarations de variables let et const sont liées contextuellement au bloc dans lequel elles sont déclarées.

 13.2.14Runtime Semantics: BlockDeclarationInstantiation ( code, env ) Note When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record. BlockDeclarationInstantiation is performed as follows using arguments code and env. code is the Parse Node corresponding to the body of the block. env is the Lexical Environment in which bindings are to be created. 1. Let envRec be env's EnvironmentRecord. 2. Assert: envRec is a declarative Environment Record. 3. Let declarations be the LexicallyScopedDeclarations of code. 4. For each element d in declarations, do a. For each element dn of the BoundNames of d, do i. If IsConstantDeclaration of d is true, then 1. Perform ! envRec.CreateImmutableBinding(dn, true). ii. Else, 1. Perform ! envRec.CreateMutableBinding(dn, false). b. If d is a FunctionDeclaration, a GeneratorDeclaration, an AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then i. Let fn be the sole element of the BoundNames of d. ii. Let fo be the result of performing InstantiateFunctionObject for d with argument env. iii. Perform envRec.InitializeBinding(fn, fo). 

Remarque: comme dans les deux premières variantes de l'appel de la boucle for , il n'y avait pas de telles déclarations, il n'était pas nécessaire de créer un nouvel environnement pour elles.

Nous allons plus loin et considérons ce qu'est ForBodyEvaluation :

 13.7.4.8Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ) The abstract operation ForBodyEvaluation with arguments test, increment, stmt, perIterationBindings, and labelSet is performed as follows: 1. Let V be undefined. 2. Perform ? CreatePerIterationEnvironment(perIterationBindings). 3. Repeat, a. If test is not [empty], then i. Let testRef be the result of evaluating test. ii. Let testValue be ? GetValue(testRef). iii. If ToBoolean(testValue) is false, return NormalCompletion(V). b. Let result be the result of evaluating stmt. c. If LoopContinues(result, labelSet) is false, return Completion(UpdateEmpty(result, V)). d. If result.[[Value]] is not empty, set V to result.[[Value]]. e. Perform ? CreatePerIterationEnvironment(perIterationBindings). f. If increment is not [empty], then i. Let incRef be the result of evaluating increment. ii. Perform ? GetValue(incRef). 

À quoi vous devez d'abord faire attention:
  • description des paramètres entrants:
    • test : expression vérifiée pour la vérité avant la prochaine itération du corps de boucle (par exemple: i <len );
    • increment : expression évaluée au début de chaque nouvelle itération (sauf la première) (par exemple: i ++ );
    • stmt : corps de boucle
    • perIterationBindings : variables déclarées avec let dans le premier paramètre for (par exemple: let i = 0 || let i || let i, j );
    • labelSet : étiquette de la boucle;
  • point 2: ici, si le paramètre non vide perIterationBindings est passé , un deuxième environnement est créé pour effectuer le passage initial de la boucle;
  • paragraphe 3.a: vérification d'une condition donnée pour poursuivre l'exécution du cycle;
  • clause 3.b: exécution du corps de cycle;
  • point 3.e: créer un nouvel environnement.

Eh bien, et, directement, l'algorithme pour créer des environnements internes de la boucle for :

 13.7.4.9Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings ) 1. The abstract operation CreatePerIterationEnvironment with argument perIterationBindings is performed as follows: 1. If perIterationBindings has any elements, then a. Let lastIterationEnv be the running execution context's LexicalEnvironment. b. Let lastIterationEnvRec be lastIterationEnv's EnvironmentRecord. c. Let outer be lastIterationEnv's outer environment reference. d. Assert: outer is not null. e. Let thisIterationEnv be NewDeclarativeEnvironment(outer). f. Let thisIterationEnvRec be thisIterationEnv's EnvironmentRecord. g. For each element bn of perIterationBindings, do i. Perform ! thisIterationEnvRec.CreateMutableBinding(bn, false). ii. Let lastValue be ? lastIterationEnvRec.GetBindingValue(bn, true). iii. Perform thisIterationEnvRec.InitializeBinding(bn, lastValue). h. Set the running execution context's LexicalEnvironment to thisIterationEnv. 2. Return undefined. 

Comme nous pouvons le voir, le premier paragraphe vérifie la présence de tout élément dans le paramètre passé, et le paragraphe 1 n'est effectué que s'il y a des annonces let . Tous les nouveaux environnements sont créés en référence au même contexte externe et prennent les dernières valeurs de l'itération précédente (environnement de travail précédent) comme nouvelles liaisons de variables let .

À titre d'exemple, considérons une expression similaire:

 let arr = []; for (let i = 0; i < 3; i++) { arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

Et voici comment il peut être décomposé sans utiliser for (avec une certaine conventionnalité):

 let arr = []; //    { let i = 0; //     for } //   ,   { let i = 0; //    i    if (i < 3) arr.push(i); } //    { let i = 0; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 1; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 2; //    i    i++; if (i < 3) arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

En fait, nous arrivons à la conclusion que pour chaque contexte, et ici nous en avons cinq, nous faisons de nouvelles liaisons pour les variables let déclarées comme premier paramètre de for (important: cela ne s'applique pas aux déclarations let directement dans le corps de la boucle).

Voici à quoi ressemble, par exemple, cette boucle lors de l'utilisation de var lorsqu'il n'y a pas de liaisons supplémentaires:

 let arr2 = []; var i = 0; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); console.log(arr); // Array(3) [ 0, 1, 2 ] 

Et nous pouvons arriver à une conclusion apparemment logique que si, pendant l'exécution de nos boucles, il n'est pas nécessaire de créer des liaisons distinctes pour chaque itération ( plus sur les situations dans lesquelles cela, au contraire, peut avoir du sens ), nous devons faire la déclaration des variables incrémentielles avant avec une boucle for , qui devrait nous éviter de créer et de supprimer un grand nombre de contextes et, en théorie, d'améliorer les performances.

Essayons de le faire, en utilisant le même tri d'un tableau de 100 000 éléments comme exemple, et pour des raisons de beauté, nous faisons également la définition de toutes les autres variables avant pour :

 let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 34.246 . //   Chrome: 10.803 . 

Résultat inattendu ... Tout le contraire de ce qui était attendu, pour être précis. Le inconvénient de Firefox dans ce test est particulièrement frappant.

Ok Cela n'a pas fonctionné, retournons la déclaration des variables i et j aux paramètres des cycles correspondants:

 let min, mini, len = arr.length; for (let i = 0; i < len-1; i++) { mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 6.575 . //   Chrome: 6.749 . 

Hm. Il semblerait, techniquement, que la seule différence entre le dernier exemple et l'exemple au début de l'article soit les déclarations faites des variables min, mini et len en dehors de la boucle for , et bien que la différence soit toujours contextuelle, elle n'est pas très intéressante pour nous maintenant, et, en plus nous nous sommes débarrassés de la nécessité de déclarer ces variables 99 999 fois dans le corps du cycle de niveau supérieur, ce qui, en théorie, devrait augmenter la productivité plutôt que la réduire de plus d'une seconde.

Autrement dit, il s'avère qu'en quelque sorte, travailler avec des variables déclarées dans le paramètre ou le corps de la boucle for se produit beaucoup plus rapidement qu'en dehors de celui-ci.

Mais, nous ne semblions pas voir d'instructions «turbo» dans la spécification de la boucle for qui pourraient nous conduire à une telle idée. Par conséquent, ce ne sont pas les spécificités du travail de la boucle for en particulier, mais autre chose ... Par exemple, les caractéristiques des déclarations let : quelle est la principale caractéristique qui distingue let de var ? Contexte d'exécution des blocs! Et dans nos deux derniers exemples, nous avons utilisé des publicités en dehors du bloc. Mais, que se passe-t-il si au lieu de déplacer ces déclarations vers for, nous sélectionnons simplement un bloc séparé pour elles?

 { let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } //   Firefox: 5.262 . //   Chrome: 5.405 . 

Voila! Il s'avère que le hic, c'est que les annonces de location ont eu lieu dans un contexte mondial, et dès que nous leur avons alloué un bloc distinct, tous les problèmes ont disparu là.

Et ici, il serait bon de rappeler une autre façon, légèrement injustifiée, de déclarer des variables - var .

Dans l'exemple au début de l'article, le temps de tri à l'aide de var a montré un résultat extrêmement déplorable par rapport à let . Mais, si vous regardez de plus près cet exemple, vous pouvez constater que, comme var n'avait pas de liaisons de blocs de variables, le contexte réel des variables était global. Et nous, sur l'exemple de let , avons déjà découvert comment cela peut affecter les performances (et, ce qui est typique, lorsque vous utilisez let , le rabattement de la vitesse s'est avéré être plus fort que dans le cas de var , en particulier dans Firefox ). Par conséquent, pour être juste, nous allons exécuter un exemple avec var créant un nouveau contexte pour les variables:

 function test() { var i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.255 . //   Chrome: 5.411 . 

Et, nous avons obtenu le résultat presque identique à ce qui était lors de l'utilisation de let .

Enfin, vérifions si le ralentissement se produit en lisant la variable globale sans changer sa valeur.

laisse

 let len = arr.length; for (let i = 0; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.262 . //   Chrome: 5.391 . 

var

 var len = arr.length; function test() { var i, j, min, mini; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.258 . //   Chrome: 5.439 . 

Les résultats indiquent que la lecture de la variable globale n'a pas affecté le temps d'exécution.

Pour résumer


  1. Changer les variables globales est beaucoup plus lent que changer les variables locales. En tenant compte de cela, il est possible d'optimiser le code dans des situations appropriées en créant un bloc ou une fonction séparée, y compris pour déclarer des variables, au lieu d'exécuter une partie du code dans un contexte global. Oui, dans presque tous les manuels, vous pouvez trouver des recommandations pour créer le moins de liaisons globales possible, mais généralement, seul un encrassement de l'espace de noms global est indiqué comme raison, et pas un mot sur les problèmes de performances possibles.
  2. Malgré le fait que l'exécution de boucles avec une déclaration let dans le premier paramètre for crée un grand nombre d'environnements, cela n'a presque aucun effet sur les performances, contrairement aux situations où nous prenons de telles déclarations en dehors du bloc. Néanmoins, il ne faut pas exclure la possibilité de l'existence de situations exotiques lorsque ce facteur affectera plus significativement la productivité.
  3. Les performances des variables var ne sont toujours pas inférieures à celles des variables let , cependant, elles ne les dépassent pas (encore une fois, dans le cas général), ce qui nous amène à la conclusion suivante qu'il n'y a aucune raison d'utiliser les déclarations var sauf à des fins de compatibilité. Cependant, s'il est nécessaire de manipuler des variables globales en changeant leurs valeurs, la variante avec var en termes de performances sera préférable (au moins pour le moment, si, en particulier, on suppose que le script peut également être exécuté sur le moteur Gecko).

Les références


ECMAScript 2019 (ECMA-262)
Utilisation des déclarations let des variables et des fonctionnalités des fermetures résultantes en JavaScript

Source: https://habr.com/ru/post/fr469523/


All Articles