代码性能对JavaScript中变量声明上下文的依赖


最初,本文被认为是其自用的一个小型基准,并且通常不打算成为本文,但是,在进行测量的过程中, JavaScript体系结构的实现中浮现出一些有趣的功能,这些功能在某些情况下会严重影响最终代码的性能。 我建议和您熟悉所获得的结果,同时还要分析一些相关主题:循环,环境(执行上下文)和块。

在我的文章“使用let变量声明和所产生的JavaScript闭包的功能”的结尾我简要地谈到了比较循环中let(LexicalDeclaration)var(VarDeclaredNames)变量声明的性能的主题。 为了进行比较,我们使用了手动运行时(无需Array.prototype.sort()的帮助)对数组进行排序,最简单的方法之一就是通过选择进行排序,因为数组长度为100,000,我们得到的略多于50亿。 在两个循环(外部循环和嵌套循环)中进行迭代,并且此数字最终应允许进行足够的评估。

对于var,它正在对视图进行排序:

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 . 

并且

 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 . 

看到这些数字,似乎可以毫不含糊地争论说, 广告的速度完全超过var 。 但是,除了这个结论之外,还有一个悬而未决的问题:如果将let声明放在for循环之外会发生什么?

但是,在执行此操作之前,您需要在当前ECMAScript 2019规范(ECMA-262)的指导下深入研究for循环的工作:

 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). 
注意:在源代码中,IterationStatement之后的冒号未用撇号括起来-在此处添加了冒号,因此没有自动格式化会大大破坏文本的可读性。

正如我们所看到的,这里有三个选项可供调用和for循环的进一步工作:
  • with for(表达式;表达式;表达式)语句
    ForBodyEvaluation (第二个Expression,第三个Expression,Statement,“”,labelSet)
  • for (varVariableDeclarationList; Expression; Expression)语句一起使用
    ForBodyEvaluation (第一个Expression,第二个Expression,Statement,“”,labelSet)。
  • at for (LexicalDeclarationExpression; Expression)语句
    ForBodyEvaluation (第一个表达式,第二个表达式,语句,perIterationLets,labelSet)

在最后的第三个变量中,与前两个变量不同,第四个参数不为空-perIterationLets-这些实际上是传递给for循环的第一个参数中的let声明。 它们在第10段中指定:
-如果isConstfalse ,则让perIterationLets为boundNames; 否则,让perIterationLets为“”。
如果将常量传递给for ,而不是传递给变量,则perIterationLets参数将为空。

另外,在第三个选项中,有必要注意第2段:
-让loopEnv为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. 

在此,作为参数E ,采用从中调用for循环的环境(全局,任何函数等),并创建一个新环境以参考创建它的外部环境执行for循环(第4点)。 由于环境是执行的上下文,因此我们对此事实感兴趣。

我们还记得letconst变量声明在上下文中绑定到了声明它们的块。

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

注意:由于在调用for循环的前两个变体中没有此类声明,因此无需为它们创建新的环境。

我们进一步考虑一下什么是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). 

首先要注意的是:
  • 传入参数的描述:
    • test :在循环体的下一次迭代之前检查表达式是否为真(例如: i <len );
    • 增量 :在每个新迭代的开始(第一个迭代除外)开始时评估的表达式(例如: i ++ );
    • stmt :循环体
    • perIterationBindings :在第一个for参数中用let声明的变量(例如: let i = 0 || let i || let i,j );
    • labelSet :循环的标签;
  • 要点2:这里,如果传递了非空参数perIterationBindings ,则会创建第二个环境来执行循环的初始传递;
  • 第3.a段:检查给定条件以继续执行周期;
  • 第3.b条:执行循环体;
  • 3.e点:创建一个新环境。

那么,直接地,用于创建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. 

如我们所见,第一段检查传递的参数中是否存在任何元素,而只有在有let公告的情况下才执行第一段。 所有新环境都是参照相同的外部上下文创建的,并采用前一次迭代的最新值(先前的工作环境)作为let变量的新绑定。

例如,考虑一个类似的表达式:

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

这是不使用for即可分解的方式 (具有一定的常规性):

 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 ] 

实际上,我们得出的结论是,对于每个上下文,这里有五个,我们为在变量for中声明为第一个参数的let变量建立新的绑定(重要:这不适用于直接在循环体内的let声明)。

例如,这是在没有附加绑定的情况下使用var时此循环的外观:

 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 ] 

我们可以得出一个看似合乎逻辑的结论,即如果在循环执行期间不需要为每次迭代创建单独的绑定( 相反,这可能更有意义 ),我们应该在声明增量变量之前使用for循环,这可以避免我们创建和删除大量上下文,并且从理论上讲可以提高性能。

让我们尝试以相同的方式对100,000个元素的数组进行排序,以达到此目的。为了美观,我们还为for定义了所有其他变量:

 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 . 

出乎意料的结果...确切地说,与预期相反。 这项测试中的Firefox下降特别引人注目。

好啦 这没有用,让我们将ij变量的声明返回到相应循环的参数:

 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 . 

嗯 从技术上讲,最后一个示例与本文开头的示例之间的唯一区别似乎是在for循环外对变量min,minilen进行了声明,尽管该区别仍是上下文相关的,但对于我们而言,这并不是很有趣,此外,我们摆脱了在上层循环的主体中将这些变量声明为99,999次的需要,从理论上讲,这又应该提高生产率,而不是将生产率降低一秒钟以上。

也就是说,事实证明,以某种方式处理在for循环的参数或主体中声明的变量比在其外部进行处理要快得多。

但是,我们似乎没有在for循环的规范中看到任何“ turbo”指令会导致我们产生这种想法。 因此,不是for循环的具体工作,而是其他一些事情。例如, let声明的功能: letvar的主要区别是什么? 阻止执行上下文! 在我们的最后两个示例中,我们在块外使用了广告。 但是,如果不将这些声明移回原处而只是为它们选择一个单独的块怎么办?

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

瞧! 事实证明,要注意的是, 公告是在全球范围内进行的,一旦我们为它们分配了单独的区域,所有问题就在那里消失了。

在这里,最好回忆起另一种稍微不受欢迎的声明变量的方法var

在本文开头的示例中,相对于let ,使用var进行排序的时间显示出极其糟糕的结果。 但是,如果您仔细查看此示例,可能会发现,由于var没有变量块绑定,因此变量的实际上下文是全局的。 而且,在let的示例中, 我们已经发现了它如何影响性能(而且,通常,使用let时 ,速度下降的结果要比使用var的情况要强,尤其是在Firefox中 )。 因此,为了公平起见,我们将执行一个示例,其中var为变量创建了新的上下文:

 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 . 

并且,我们得到的结果几乎与使用let时的结果相同。

最后,让我们通过读取全局变量而不更改其值来检查减速是否发生。



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

结果表明,读取全局变量不会影响执行时间。

总结一下


  1. 更改全局变量比更改局部变量要慢得多。 考虑到这一点,可以通过创建单独的块或函数(包括用于声明变量)而不是在全局上下文中执行部分代码来在适当情况下优化代码。 是的,在几乎所有教科书中,您都可以找到有关建议建立尽可能少的全局绑定的建议,但通常仅表示阻塞全局名称空间是一个原因,而不是任何有关可能的性能问题的字眼。
  2. 尽管实际上在第一个for参数中使用let声明执行循环会创建大量环境,但与将此类声明放在块外的情况不同,这几乎对性能没有影响。 然而,当这一因素将极大地影响生产力时,不应排除存在异国情调的可能性。
  3. var变量的性能仍然不逊于let变量,但是它没有超过let变量(同样,在一般情况下),这使我们得出下一个结论除了出于兼容性目的,没有理由使用var声明。 但是,如果需要通过更改全局变量的值来操作全局变量,则在性能方面具有var的变体将是更好的选择(至少在目前,尤其是假设脚本也可以在Gecko引擎上运行)。

参考文献


ECMAScript 2019(ECMA-262)
在JavaScript中使用变量的let声明和结果闭包的功能

Source: https://habr.com/ru/post/zh-CN469523/


All Articles