你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
StrayBugs

没必要缓存 for 循环中的 Array.length 13个回复 专栏 @ Javascript

StrayBugs 发布于 2 年前

问题

缓存 Array.length 是老生常谈的小优化。

// 不缓存 
for (var i = 0; i < arr.length; i++) {
...
}

// 缓存
var len = arr.length;
for (var i = 0; i < len; i++) {
...
}

// 或者
for (var i = 0, len = arr.length; i < len; i++) {
...
}

但以前写过 Java 的笔者一直对这种破碎的写法感到不适,也对这种写法的实际优化效果产生疑问。

且推崇这种写法的朋友似乎很多也是“前辈这么说+自己想了一下觉得有道理”。

由于 for 循环搭配 Array.length 是极度常用的 JavasScript 代码,所以还是有必要搞清楚的。

结论

经过一番摸索后笔者得到的结论是:缓存 Array.lengh 对优化的影响没有想象中的大,甚至可能会有所减慢。

理由

从测试结果上看

stackoverflow 上也有这个讨论,For-loop performance: storing array length in a variable

accepted 的答案是说缓存会起到加速的结果,给出了 jsPerf 测试。

但是有答案反对,也给出了 jsPerf 测试。

两个答案的区别在于“循环不变量代码移动(Loop-invariant code motion)”,accepted 答案的测试循环里没有访问到数组,是不实际的,后面会讲到。

从另一篇文章 Shoud I have to cache my array’s length? 的测试结果也可以看出缓存差别不大。

1

还有这篇 JavaScript's .length Property is a Stored Value

2

从 V8 的中间代码分析

这篇文章 How the Grinch stole array.length access 从 V8 的 hydrogen 探讨 Array.length 在 for 循环中的处理。

正如上面提到的“循环不变量代码移动”,V8 引擎会聪明的把能确定不变的代码移到循环外。

所以像下面这种代码也不会影响引擎对 Array.length 的优化:

function uncached(arr) {
  for (var i = 0; i < arr.length; i++) {
    arr[i]
  }
}

而当在循环中调用函数时,V8 会尝试将函数进行内联,从而继续进行“循环不变量代码移动”优化。
但当函数不可内联时,V8 就没辙了,每次循环都要重新计算一遍 length

function BLACKHOLE(sum, arr) {
  try { } catch (e) { }
}

function uncached(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
    if (sum < 0) BLACKHOLE(arr, sum);
  }
  return sum;
}

但这时即便是在循环外缓存了 length 也是没有用的,引擎没法预判数组的变化,当需要访问数组元素时会触发 bounds check ,从而照样要计算一遍 length 。所以缓存 length 是没有用的。

甚至,由于多了一个变量,底层的寄存器分配器每次循环还要多一次恢复这个变量。当然这个只有在大规模的情况下才会看出区别。

结尾

当然这篇文章也有局限性,仅仅讨论了 V8 引擎,也没有讨论访问 length 代价更高的 HTMLCollection 。但这已经足够说明两者差别不大,一般情况下我们不用再局限于缓存的写法,可以放开来按照自己喜欢的方式去写循环了。

【完】

  • 小小小无路

    写的不错很稀饭,赞一个

    #1
  • 柒柒

    优化从细节之处着手

    #2
  • hefangshi

    在现代VM和编译器的优化下,很多想当然的优化写法实际上已经没有必要了,不过也因此造成黑盒严重,很多问题只能靠benchmark来研究。

    #3
  • vvian00

    看到反常识的标题就滚进来了,果然又是引擎优化的功劳

    #4
  • 前端农民工

    缓存length主要是应用在HTMLCollection的length查询上,有一定的性能差距

    #5
  • rambo

    已经养成将长度提取成一个变量的习惯了。 不过最近一直用的是
    es5 原生支持的 forEach map Object.keys() 来循环

    个人认为将其提取成一个变量。还是有一定好处的。
    首先代码可读性较高。 ---这点差异。跟代码可读性比起来真的不需要考虑吧
    其次他更多的优化是V8自己的优化。 别的引擎未必会做到一致

    #6
  • 吴双Orange

    缓存长度已经习惯了

    #7
  • fansekey

    我写多种语言,所以,我还是会缓存长度。

    #8
  • jimnox

    当然不缓存变量,不美了,代码快那一点点又有啥用。 不美了

    #9
  • skylerzhang

    从来没缓存过长度...

    #10
  • ziyan

    赞一个,技术上的东西得深入思考,不能安于习惯

    #11
  • gaubee

    题外话,有一种考虑是这样的:
    1. 这个数组的内容可控,能确保item不为null、undefined、0、""之类的。
    2. 考虑代码量。
    所以有有这种写法:

    var arr = [...]
    for(var item, i=0; item = arr[i]; i+=1){
      log(item)
    }
    
    #12
  • JsonYoung

    HTMLCollection的循环还是需要缓存的,万一for里面有元素的插入不是就死循环了,,,以养成缓存习惯+1

    #13
登录后回复,如无账号,请使用邀请码注册