【问题标题】:Javascript nested function call optimizationJavascript嵌套函数调用优化
【发布时间】:2013-07-11 02:38:19
【问题描述】:

编辑!在大量后续研究表明我的问题没有一个简单的答案后,我将答案更改为我自己的答案。见下文!

因此,在我上一个问题的后续行动中,我试图更好地处理最佳 Javascript 实践以优化性能。对于以下示例,我正在使用浏览器内分析器在 Chrome 28.0.1500.70 中进行测试。

我已经将一些数学函数封装在一个对象中,这些函数每秒被调用几百 k 次,并试图减少一些执行时间。

我已经通过将父对象的本地副本作为被调用函数本身的本地对象进行了一些优化,并获得了不错的 (~16%) 性能提升。然而,当我从父对象调用另一个函数时,我得到了巨大的(~100%)性能提升。

最初的设置是 calcNeighbors 通过 this.cirInd 调用同伴父对象函数 cirInd。

制作 cirInd 的本地 var 副本并调用它会带来巨大的性能提升,执行时间少于之前 calcNeighbors 的一半。

但是,将 cirInd 设为 calcNeighbors 中的内联函数会导致性能恢复到与从父对象调用它时相同的速度。

我真的对此感到困惑。我想这可能是 Chrome 分析器中的一个怪癖(在第二种情况下,cirInd 根本没有出现)但是当我使用案例 2 时,应用程序的性能肯定会显着提升。

有人可以解释为什么案例 2 比案例 1 快得多,但更重要的是,为什么案例 3 似乎没有任何性能提升?

有问题的函数在这里:

从父对象调用:

  window.bgVars = {
     <snip>
     "cirInd": function(index, mod){
        //returns modulus, array-wrapping value to implement circular array
        if(index<0){index+=mod;}
        return index%mod;
     },
     "calcNeighbors": function(rep){
        var foo = this.xBlocks;
        var grid = this.cGrid;
        var mod = grid.length;
        var cirInd = this.cirInd;
        var neighbors = grid[this.cirInd(rep-foo-1, mod)] + grid[this.cirInd(rep-foo, mod)] + grid[this.cirInd(rep-foo+1, mod)] + grid[this.cirInd(rep-1, mod)] + grid[this.cirInd(rep+1, mod)] + grid[this.cirInd(rep+foo-1, mod)] + grid[this.cirInd(rep+foo, mod)] + grid[this.cirInd(rep+foo+1, mod)];
        return neighbors;
     },
     <snip>
  }

通过局部变量调用:

  window.bgVars = {
     <snip>
     "cirInd": function(index, mod){
        //returns modulus, array-wrapping value to implement circular array
        if(index<0){index+=mod;}
        return index%mod;
     },
     "calcNeighbors": function(rep){
        var foo = this.xBlocks;
        var grid = this.cGrid;
        var mod = grid.length;
        var cirInd = this.cirInd;
        var neighbors = grid[cirInd(rep-foo-1, mod)] + grid[cirInd(rep-foo, mod)] + grid[cirInd(rep-foo+1, mod)] + grid[cirInd(rep-1, mod)] + grid[cirInd(rep+1, mod)] + grid[cirInd(rep+foo-1, mod)] + grid[cirInd(rep+foo, mod)] + grid[cirInd(rep+foo+1, mod)];
        return neighbors;
     },
     <snip>
  }

内联调用:

  window.bgVars = {
     <snip>
     "calcNeighbors": function(rep){
        var foo = this.xBlocks;
        var grid = this.cGrid;
        var mod = grid.length;
        function cirInd(index, mod){
          //returns modulus, array-wrapping value to implement circular array
          if(index<0){index+=mod;}
          return index%mod;
        }
        var neighbors = grid[cirInd(rep-foo-1, mod)] + grid[cirInd(rep-foo, mod)] + grid[cirInd(rep-foo+1, mod)] + grid[cirInd(rep-1, mod)] + grid[cirInd(rep+1, mod)] + grid[cirInd(rep+foo-1, mod)] + grid[cirInd(rep+foo, mod)] + grid[cirInd(rep+foo+1, mod)];
        return neighbors;
     },
     <snip>
  }

【问题讨论】:

  • “为什么案例 3 似乎没有带来任何性能提升” --- 为什么将函数移动到相同的范围会带来一些性能提升?
  • 不是 100% 相关但仍然有用:developers.google.com/v8/design#prop_access
  • #3 每次调用都会生成一个新函数 cirInd(),而 #2 每次调用都会回收同一个函数。更少的激活创建 = 更快的运行时间和更少的垃圾清理。

标签: javascript performance


【解决方案1】:

也许在简化视图中看到 #2 和 #3 将有助于说明对象创建的副作用。

我相信这应该很明显:

alls1=[];
alls2=[];

function inner1(){}
function outer1(){
     if(alls1.indexOf(inner1)===-1){ alls1.push(inner1); }
}


function outer2(){
   function inner2(){}
   if(alls2.indexOf(inner2)===-1){ alls2.push(inner2); }
}

for(i=0;i<10;i++){
   outer1();
   outer2();
}

alert([ alls1.length, alls2.length  ]); // shows: 1, 10

函数是对象,创建新对象从来都不是免费的。

编辑:在 #1 与 #2 上展开

再一次,一个简化的例子将有助于说明:

function y(a,b){return a+b;}
var out={y:y};
var ob={
   y:y, 
   x1: function(a){ return this.y(i,a);},
   x2: function(a){ return y(i,a);},
   x3: function(a){ return out.y(i,a);}
}

var mx=999999, times=[], d2,d3,d1=+new Date;
for(var i=0;i<mx;i++){ ob.x1(-i) }
times.push( (d2=+new Date)-d1 );

for(var i=0;i<mx;i++){ ob.x2(-i) }
times.push( (d3=+new Date)-d2 );

for(var i=0;i<mx;i++){ ob.x3(-i) }
times.push( (+new Date)-d3 );

alert(times); // my chrome's typical: [ 1000, 1149, 1151 ]

了解在一个简单的示例中存在更多噪音,并且闭包是所有开销的很大一部分3,但它们之间的差异才是重要的。

在此演示中,您不会看到在动态系统中观察到的巨大增益,但您会看到 y 和 out.y 配置文件与 this.y 相比有多接近,其他条件相同。

主要的一点是,并不是额外的点分辨率本身会减慢速度,正如一些人所暗示的那样,重要的是 V8 中的“this”关键字,否则 out.y() 会更接近这个.y()...

firefox 是另一回事。

跟踪允许 this.whatever 被预测,因此所有三个配置文件都在一个坏骰子中,在与 chrome 相同的组合上:[2548,2532,2545]...

【讨论】:

  • Derp,你说得对,我不知道为什么我没有在 #3 中发现它。我仍然有点困惑,为什么 #1 会受到如此巨大的性能影响。
  • @DanHeidel:我会提出 #2 中调用的 cirInd 没有内部 this 绑定,而它是 #1 中的整个对象。整个路径标记通常会被 JIT 快捷化。但是,使用“这个”。意味着必须在执行时重新评估持有者对象。在所有其他条件相同的情况下,使用(几乎)除“this”之外的点左侧的任何单个标识符都应该描述大致相同的词法命名函数参考。
  • 有趣。因此,如果我没听错的话,对 bgVars.cirInd 而不是 this.cirInd 的引用将允许 JIT 避免运行时查找命中?
  • 我不是低级专家,但我的理解是,如果 y 在所有三个上都相同,则 x.y() 的轮廓更像 y() 而不是 this.y()。我将编辑答案以详细说明。
  • 所以,我尝试了显式引用与使用它,在 Chrome 中,它没有提高性能 - 它看起来与 #1 没有区别。更奇怪的是,我设置了一个 jsPerf 来测试所有 4 个案例,得到了完全不同的结果。在这种情况下,#1-3 几乎相同,#4 慢了 10 倍以上。 jsperf.com/different-nested-js-function-calls
【解决方案2】:

第 1 项所涉及的时间相对较多的原因应该是显而易见的。您访问整个对象范围,然后必须找到一个属性。

数字 2 和 3 都是指向函数的指针,所以没有查找。

jsPerf 是测试这些类型情况的一个非常好的资源,我强烈建议在那里重新创建场景并运行测试以查看确切的差异以及它们是否对您很重要。

【讨论】:

  • O(n)? 我确定这是一个哈希表而不是一个列表 *.com/a/6602088/251311
  • @zerkms - 那么如果它是一个哈希值,那么访问一个属性的时间是从哪里来的呢?我从答案中删除了时间复杂度假设。
  • 查看评论更新。即使对于类似哈希的结构,它仍然比 O(1) 更昂贵
  • @zerkms - 因此对于散列,计算会创建一个键,并且对一组属性进行迭代检查以查看当前键是否是为散列生成的键。一旦找到,就会返回键索引处的值。这听起来对你吗?如果是这样,那不是 O(n),其中 n 是哈希中的键数?最坏的情况是检查每个键。
  • 对于字符串键,正确的哈希表实现的查找成本接近O(1)(这是理想的)。 O(n) 是所有密钥都落入同一个存储桶的最坏情况(V8 实现不可能)。一些有用的阅读:lemire.me/blog/archives/2009/08/18/…
【解决方案3】:

好的,我已经研究这个问题一段时间了,TL;DR - 这很复杂。

事实证明,许多性能问题确实取决于平台、浏览器甚至次要浏览器修订号。也不是一点点。 jsPerf 上有很多例子显示了诸如“for vs while”之类的东西;或者“类型数组与标准数组”在不同浏览器版本的有利执行速度方面来回摆动。这可能是由于 JIT 优化权衡。

一般性能问题的简短回答 - 只需在 jsPerf 中测试所有内容。我在这个线程中得到的建议在所有情况下都没有帮助。 JIT 使事情变得复杂。如果您有像我这样的背景并且习惯于具有某些经验法则编码模式的 C 程序,这一点尤其重要,这些编码模式往往会加快速度。不要假设任何事情 - 只是测试它。

注意:我在原始问题中列出的许多奇怪问题都是由于使用了默认的 Chrome 分析器。 (例如:您从 Ctl+Shift+I 菜单获得的分析器)如果您正在执行大量非常快速的循环(例如在图形渲染中),请不要使用此分析器。它的时间分辨率为 1 毫秒,这对于进行适当的性能调试来说太粗糙了。

事实上,案例 2 比其他案例快得多的整个问题完全是由于分析器根本没有“看到”许多函数调用并且不正确地报告 CPU 百分比。在热图中,我可以清楚地看到内部循环函数正在触发但没有被分析器记录的巨大延伸。

解决方案:http://www.html5rocks.com/en/tutorials/games/abouttracing/# Chrome 在 about:tracing 中内置了一个不太明显但功能更强大的分析器。它具有微秒级的分辨率,能够读取代码标签以进行子功能分辨率,并且通常更加出色。我一开始使用这个分析器,结果就与我在 jsPerf 上看到的一致,并帮助我将渲染时间减少了近一半。我是怎么做到的?同样,这并不简单。在某些情况下,调用子程序会有所帮助,而在其他情况下则没有。将整个渲染引擎从对象文字重构为模块模式似乎有点帮助。在 for 循环中预先计算任何乘法运算似乎确实有很大的影响。等等等等。

关于 about:tracing 分析器的快速说明:缩放和平移是使用键盘上的 ASWD 进行的 - 我花了一段时间才弄明白。此外,它会分析所有选项卡并在正在分析的页面之外的选项卡中运行。因此,尽量减少您打开的无关选项卡的数量,因为它们会使分析器视图变得混乱。此外,如果测试 Canvas 应用程序,请务必将选项卡切换到应用程序,因为 RequestAnimationFrame 通常不会在选项卡不活动且不可见时触发。

【讨论】: