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; }
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; }
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:
- with for (Expression; Expression; Expression) Statement
ForBodyEvaluation (the second Expression, the third Expression, Statement, "", labelSet) . - with for (varVariableDeclarationList; Expression; Expression) Statement
ForBodyEvaluation (the first Expression, the second Expression, Statement, "", labelSet). - at for (LexicalDeclarationExpression; Expression) Statement
ForBodyEvaluation (the first Expression, the second Expression, Statement, perIterationLets, labelSet)
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:
- description of incoming parameters:
- test : expression checked for truth before the next iteration of the loop body (for example: i <len );
- increment : expression evaluated at the beginning of each new iteration (except the first) (for example: i ++ );
- stmt : loop body
- perIterationBindings : variables declared with let in the first for parameter (for example: let i = 0 || let i || let i, j );
- labelSet : label of the loop;
- point 2: here, if the non-empty parameter perIterationBindings is passed , a second environment is created to perform the initial pass of the loop;
- paragraph 3.a: checking for a given condition for continuing the execution of the cycle;
- clause 3.b: execution of the cycle body;
- point 3.e: creating a new environment.
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);
And here is how it can be decomposed without using
for (with a certain amount of conventionality):
let arr = [];
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);
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; }
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; }
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; } }
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();
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; }
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();
The results indicate that reading the global variable did not affect the execution time.
To summarize
- 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.
- 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.
- 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