【问题标题】:Inconsistent execution times in JavascriptJavascript 中的执行时间不一致
【发布时间】:2013-07-11 09:43:08
【问题描述】:

注意:如果可以的话,我会检查 Mike Brandt 的答案,因为他发现了我在死/活像素比率上的愚蠢错误。但肯得到了普遍的好建议。

我正在尝试在 Canvas 元素中调试 Conway 的生活游戏的一些性能问题,但遇到了一些非常奇怪的性能问题。

我的速度大约为 4-12 FPS,绘图功能的基准测试表明整体性能应该能够达到 60 FPS。

下面是 Canvas 的绘制代码。 RequestAnimationFrame 以大约 30FPS 的速度调用 updateBgCanvas。整个过程正在 Chrome 28.0.1500.70 中运行和性能测试。

(对于凌乱的代码,我深表歉意,我一直在将代码分成更小的子单元,以便在性能分析器中获得更大的粒度,而不考虑良好的编码技术)

不出所料,Canvas 绘图函数(fillDead 和 fillLive 是最大的 CPU 消耗量,但这里是它变得奇怪的地方。fillLive 消耗 5-6% 的 CPU 时间(大约是我对 fillRect 基准测试所做的预期)和 fillDead占用了高达 36-38% 的 CPU 时间。除了针对 1 或 0 的条件测试之外,这些功能相同。

我已经尝试在父函数中交换调用顺序,并且用于填充和 fillDead 的颜色始终比几乎相同的 fillLive 花费 6-7 倍的时间来调用。我完全不知道为什么会这样。

有什么建议吗?

  window.bgVars = {
     "about": "The background is the famous Conway Game of Life",
     "_Canvas": {},
     "_Ctx": {},
     "xBlockSize": 5,
     "yBlockSize": 5,
     "xBlocks": 0,
     "yBlocks": 0,
     "bornVals": [3],
     "stayAliveVals": [2, 3],
     "cGrid": [],
     "cGrid2": [],
     "cL": 0,
     "initBgVars" : function(iCanvas, iCtx){
        console.log(this.xBlockSize);
        this._Canvas = iCanvas;
        this._Ctx = iCtx;
        this.cGrid = [];
        this.cGrid2 = [];
        this.xBlocks = Math.round(myCanvas.width/this.xBlockSize) + 1;
        this.yBlocks = Math.round(myCanvas.height/this.yBlockSize) + 1;
        for(var rep=0;rep<(this.xBlocks * this.yBlocks);rep++){
           this.cGrid.push(Math.round(Math.random()*0.8));
        }
        this.cGrid2.length = this.cGrid.length;
     },
     "cirInd": function(index){
        //returns modulus, array-wrapping value to implement circular array
        if(index<0){index+=this.cGrid.length;}
        return index%this.cGrid.length;
     },
     "calcNeighbors": function(rep){
        var foo = this.xBlocks;
        var neighbors = this.cGrid[this.cirInd(rep-foo-1)] + this.cGrid[this.cirInd(rep-foo)] + this.cGrid[this.cirInd(rep-foo+1)] + this.cGrid[this.cirInd(rep-1)] + this.cGrid[this.cirInd(rep+1)] + this.cGrid[this.cirInd(rep+foo-1)] + this.cGrid[this.cirInd(rep+foo)] + this.cGrid[this.cirInd(rep+foo+1)];
        return neighbors;
     },
     "refreshGrid": function(){
        for(var rep=0;rep<this.cGrid.length;rep++){
           if(Math.random()<0.0002){this.cGrid2[rep] = 1;}
           this.cGrid[rep] = this.cGrid2[rep];
        }
     },
     "lifeRules": function(rep, neighbors){
           if(this.cGrid[rep] == 1){  //stay alive rules
              for(var rep2=0;rep2<this.stayAliveVals.length;rep2++){
                 if(neighbors==this.stayAliveVals[rep2]){this.cGrid2[rep] = 1;}
              }
           }
           if(this.cGrid[rep] == 0){  //'born' rules
              for(var rep2=0;rep2<this.bornVals.length;rep2++){
                 if(neighbors==this.bornVals[rep2]){this.cGrid2[rep] = 1;}
              }
           }          
     },
     "fillDead": function(){
        for(var rep=0;rep<this.cGrid.length;rep++){
           if(this.cGrid[rep] == 0){
              this._Ctx.fillRect((rep%this.xBlocks)*this.xBlockSize, Math.floor(rep/this.xBlocks)*this.yBlockSize, this.xBlockSize, this.yBlockSize);
           }
        }          
     },
     "fillLive": function(){
        for(var rep=0;rep<this.cGrid.length;rep++){
           if(this.cGrid[rep] == 1){
              this._Ctx.fillRect((rep%this.xBlocks)*this.xBlockSize, Math.floor(rep/this.xBlocks)*this.yBlockSize, this.xBlockSize, this.yBlockSize);
           }
        }          
     },
     "updateBgCanvas": function(){
        //fill live squares
        this._Ctx.fillStyle = 'rgb(130, 0, 0)';
        this.fillLive();
        //fill dead squares
        this._Ctx.fillStyle = 'rgb(100, 0, 0)';
        this.fillDead();
        //calculate next generation to buffer
        for(var rep=0;rep<this.cGrid.length;rep++){
           //add up the live squares in the 8 neighbor blocks
           var neighbors = this.calcNeighbors(rep);
           this.cGrid2[rep] = 0;
           //implement GoL ruleset
           this.lifeRules(rep, neighbors);
        }
        //seed with random noise to keep dynamic and copy to display buffer
        this.refreshGrid();
     }
  }

Ken 建议对数学函数进行编辑,将父对象变量复制到本地变量,使数学函数的性能提高约 16%,总体提高约 4%:

     "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 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;
     },

【问题讨论】:

  • 您是否计算过在每种情况下实际执行条件代码的次数(即 cGrid 包含的 0 是否明显多于 1)?我认为您唯一期望两个函数之间的性能相同的情况是当 cGrid 具有大致相等数量的 1 和 0 时。
  • 这是可能的。我在分析时间窗口期间观察死/活像素比率,它看起来很均匀,但它可能只是一种视错觉。我会做一个快速检查..
  • 是的,我的眼睛很脏,肮脏的骗子。死/活像素比率仅为 4:1 左右,但这似乎是可能的原因。谢谢!
  • 你应该结合 fillDead 和 fillAlive 并且也许还有 updateBgCanvas:目前你通过数组三遍;也代替this.cGrid 例如。您可以使用 vars 来保存 this 查找
  • 原来都是一个函数,我把它分解成子程序来细化分析。由于活动像素和坏点的数量之间存在如此大的差异,我只是重写了它来进行死色泛光填充,并且只绘制“活动”像素。我也一直在尝试使用 vars 来降低查找成本,但并没有看到从中获得太多性能提升。

标签: javascript performance canvas


【解决方案1】:

举个例子来说明你可以做些什么来提高性能,试着用这个修改替换 fillDead 函数:

"fillDead": function(){

   /// localize to vars so interpreter doesn't
   /// have to walk of the branches all the time:
   var cGrid = this.cGrid, ctx = this._Ctx,
       xBlocks = this.xBlocks, xBlockSize = this.xBlockSize,
       yBlockSize = this.yBlockSize,
       rep = cGrid.length - 1;

   /// while loops are generally faster than for loops
   while(rep--) 
      if(cGrid[rep] == 0){
         ctx.fillRect((rep%xBlocks) * xBlockSize, ((rep/xBlocks)|0) * yBlockSize, xBlockSize, yBlockSize);
      }
   }
},
//...

这将如何表现?如果只有 1 或 2 个网格元素,您不会看到太大差异,但网格元素越多性能应该越好(无法测试,因为我没有完整的代码)。

如果您发现它提高了性能,您也应该将其应用于其他功能。您还应该看看是否可以将其中一些 this.* 放到本地变量中,而不是在父作用域中,并将它们作为参数传递给函数。

【讨论】:

  • 关于使用本地变量的好建议。我曾假设,因为它是对本地对象的引用,所以查找时间会很短,但我在数学部分的性能提高了 16%。 (只有大约 4% 的总性能提升,但仍然是一种获得更快速度的廉价方法。(最后在上面进行编辑) 至于 vs while,JSPerf 上的最新结果似乎表明 for 实际上比 while 这些快一点天,但差异很小。
  • @DanHeidel 是的,JS 引擎在不断改进。过去快/慢的事情现在在性能上变得非常平等。至于引用时间,封闭起来确实更快,但是解释器还是要遍历分支上的所有属性。如果你有两个分支(this.branch.*),它也必须在那里进行,这在时间上是指数级的(远离可能的解释器优化)。
  • @DanHeidel 还要记住,对象是 self 在当前状态下有很多分支(很多可能是不必要的)。如果您将操作拆分为许多小的内部函数(如果在您的场景中可能的话),这些函数在内部连续调用,您也会获得一些性能提升。
  • 感谢您的提示!我已经习惯了嵌入式系统中的 C 语言,浏览器中所有的自动化抽象和幕后的东西都需要一些时间来适应。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-20
  • 1970-01-01
相关资源
最近更新 更多