Dependence of code performance on the context of variable declaration in JavaScript







Initially, this article was conceived as a small benchmark for its own use, and in general it was not planned to be an article, however, in the process of taking measurements, some interesting features surfaced in the implementation of the JavaScript architecture, which strongly affect the performance of the final code in some cases. I suggest, and you, get acquainted with the results obtained, along the way also analyzing some related topics: for loops, environment (execution context) and blocks.



At the end of my article, “Using let variable declarations and features of the resulting JavaScript closures,” I casually touched on the topic of comparing the performance of let (LexicalDeclaration) and var (VarDeclaredNames) variable declarations in loops. For comparison, we used the runtime of manual (without the help of Array.prototype.sort () ) sorting the array, since with its length of 100,000 we got a little more than 5 billion. iterations in two cycles (external and nested), and, this amount should allow an adequate assessment in the end.



For var, it was a sort of the form:



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





And for let :



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





Seeing these figures, it would seem, it can be unequivocally argued that let ads completely exceed var in speed. But, in addition to this conclusion, the question remained suspended in the air, but what will happen if we put let declarations outside of for loops?



But, before doing this, you need to delve deeper into the work of the for loop, guided by the current specification of 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).
      
      





Here, as we see, there are three options for calling and further work of the for loop:



In the last, third option, unlike the first two, the 4th parameter is not empty - perIterationLets - these are actually the same let- declarations in the first parameter passed to the for loop. They are specified in paragraph 10:

- If isConst is false , let perIterationLets be boundNames; otherwise let perIterationLets be "".

If a constant was passed to for , but not a variable, the perIterationLets parameter becomes empty.



Also, in the third option, it is necessary to pay attention to paragraph 2:

- Let loopEnv be 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.
      
      





Here, as the parameter E , the environment from which the for loop was called (global, some function, etc.) is taken, and a new environment is created to execute the for loop with reference to the external environment that created it (point 4). We are interested in this fact due to the fact that the environment is a context of execution.



And we remember that let and const variable declarations are contextually bound to the block in which they are declared.



 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).
      
      





note: since in the first two variants of calling the for loop there were no such declarations, there was no need to create a new environment for them.



We go further and consider what ForBodyEvaluation is :



 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).
      
      





What you should first pay attention to:



Well, and, directly, the algorithm for creating internal environments of the for loop:



 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.
      
      





As we can see, the first paragraph checks for the presence of any elements in the passed parameter, and paragraph 1 is only performed if there are let announcements. All new environments are created with reference to the same external context and take the latest values ​​from the previous iteration (previous working environment) as new bindings of let variables.



As an example, consider a similar expression:



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





And here is how it can be decomposed without using for (with a certain amount of conventionality):



 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 ]
      
      





In fact, we come to the conclusion that for each context, and here we have five of them, we make new bindings for let variables declared as the first parameter in for (important: this does not apply to let declarations directly in the body of the loop).



Here's how, for example, this loop will look when using var when there are no additional bindings:



 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 ]
      
      





And we can come to a seemingly logical conclusion that if during the execution of our loops there is no need to create separate bindings for each iteration ( more about situations in which this, on the contrary, may make sense ), we should make the declaration of incremental variables before with a for loop, which should save us from creating and deleting a large number of contexts and, in theory, improve performance.



Let's try to do this, using the same sorting of an array of 100,000 elements as an example, and for the sake of beauty, we will also make the definition of all other variables before for :



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





Unexpected result ... Just the opposite of what was expected, to be precise. The Firefox drawdown in this test is particularly striking.



OK. This did not work, let's return the declaration of i and j variables back to the parameters of the corresponding loops:



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





Hm. It would seem, technically, the only difference between the last example and the example at the beginning of the article is the made declarations of the variables min, mini, tmp and len outside the for loop, and although the difference is still contextual, it is not of special interest to us now, and, except Moreover, we got rid of the need to declare these variables 99,999 times in the body of the upper level cycle, which again, in theory, should increase productivity rather than reduce it by more than a second.



That is, it turns out that somehow, working with variables declared in the parameter or body of the for loop happens much faster than outside it.



But, we did not seem to see any “turbo” instructions in the specification for the for loop that could lead us to such a thought. Therefore, it’s not the specifics of the work of the for loop, but something else ... For example, the features of let declarations: what is the main feature that distinguishes let from var ? Block execution context! And in our last two examples, we used ads outside the block. But, what if instead of moving these declarations back to for, we just select a separate block for them?



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





Voila! It turns out that the catch was that let announcements took place in a global context, and as soon as we allocated a separate block for them, all the problems disappeared right there.



And here it would be nice to recall another, somewhat undeservedly cursed way of declaring variables - var .



In the example at the beginning of the article, sorting time using var showed an extremely deplorable result, relative to let . But, if you take a closer look at this example, you may find that, due to the absence of variable block bindings in var , the actual context of the variables was global. And we, on the example of let , have already discovered how this can affect performance (and, which is typical, when using let , the drawdown in speed turned out to be stronger than in the case with var , especially in Firefox ). Therefore, in fairness, we will execute an example with var creating a new context for variables:



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





And, we got the result almost identical to what was when using let .



Finally, let's check whether the slowdown occurs by reading the global variable without changing its value.



let



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





var



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





The results indicate that reading the global variable did not affect the execution time.



To summarize



  1. Changing global variables is much slower than changing local ones. Taking this into account, you can optimize the code in appropriate situations by creating a separate block or function, including for declaring variables, instead of executing part of the code in a global context. Yes, in almost any textbook you can find recommendations for making as few global bindings as possible, but usually only a clogging of the global namespace is indicated as a reason, and not a word about possible performance problems.
  2. Despite the fact that the execution of loops with a let declaration in the first for parameter creates a large number of environments, this has almost no effect on performance, unlike situations when we take such declarations outside the block. Nevertheless, one should not exclude the possibility of the existence of exotic situations when this factor will affect productivity more significantly.
  3. The performance of var variables is still not inferior to that of let variables, however, it does not exceed them (again, in the general case), which leads us to the next conclusion that it is not advisable to use var declarations except for compatibility purposes. However, if you need to manipulate global variables with changing their values, the var variant will be preferable in terms of performance (at least for the moment, if, in particular, it is assumed that the script can also be run on the Gecko engine).


References



ECMAScript 2019 (ECMA-262)

Using let declarations of variables and features of the resulting closures in JavaScript



All Articles