现代JavaScript JIT编译器在循环中是否需要缓存数组长度?

13

我认为在 for 循环中缓存数组的 length 属性是不太好的做法。例如:

for (var i = 0, l = myArray.length; i < l; ++i) {
    // ...
}
在我看来,与简单明了的方式相比,这样做会大大降低可读性。
for (var i = 0; i < myArray.length; ++i) {
    // ...
}

(更不用说因词法作用域和提升的特性而导致将另一个变量泄露到周围函数中。)

我想告诉那些这样做的人:“别费劲了;现代JS JIT编译器已经优化掉了这个技巧。” 显然,这并不是一项微不足道的优化,因为您可以在迭代数组时修改它,但我认为鉴于JIT编译器和其运行时分析技巧所涉及的所有疯狂事情,他们已经到达了这个地步。

有没有证据证明某种方式呢?

是的,我也希望能够说“这是一种微观优化;在您进行剖析之前请勿执行。” 但并不是每个人都会听取这种理由,特别是当缓存长度成为习惯,并且他们自动地执行它们时,几乎成为一种风格选择。


1
V8似乎将长度放在一个寄存器中,并始终与其进行比较,因此与自己放置在变量中没有什么不同。 - alex
@alex 没错,那你为什么要这样做呢? - Ruan Mendes
1
@JuanMendes 我并不是主张你自己来做缓存。如果我的表述不够清晰,我向你道歉。 - alex
3个回答

12

这要取决于以下几个因素:

  • 您是否已经证明您的代码在循环中花费了大量时间
  • 您完全支持的最慢的浏览器是否受益于数组长度缓存
  • 您或者在您的代码上工作的人是否认为数组长度缓存难以阅读

从我看到的基准测试结果来看(例如,这里这里),IE<9(通常是您需要处理的最慢的浏览器)的性能会从缓存数组长度中受益,所以值得这样做。就我个人而言,我长期以来习惯于缓存数组长度,并且因此发现它很容易阅读。还有其他一些循环优化措施可能会产生影响,例如逆序计数。

这是关于JSMentors邮件列表讨论的相关内容:http://groups.google.com/group/jsmentors/browse_thread/thread/526c1ddeccfe90f0


嗯,令人失望的是除了 Chrome 以外,在其他浏览器上差异似乎相当明显。但是感谢提供链接! - Domenic
老实说,我很惊讶倒计时与顺序计数相比现在竟然能带来任何可测量的差异。 - Flash
@Andrew:在现代浏览器上可能不需要这样的优化,但需要这种优化的浏览器是最慢的浏览器,也就是IE 6-8。 - Tim Down

5

我的测试显示,所有主要的新版浏览器都缓存数组的长度属性。除非你担心IE6或7,否则不需要自己缓存它。然而,自从那些日子以来,我一直在使用另一种迭代方式,因为它给了我另一个好处,我将在以下示例中描述:

var arr = ["Hello", "there", "sup"];
for (var i=0, str; str = arr[i]; i++) {
  // I already have the item being iterated in the loop as 'str'
  alert(str);
}

您必须意识到,如果数组允许包含“假值”(例如 0、false、null、undefined 或空字符串),则此迭代样式将停止运行,因此在这种情况下不能使用该样式。


1
如果数组中有空字符串,那么这段代码会出错。如果没有空字符串,你可以进一步简化它:for (var i=0, str; str = arr[i++]; ) { - Tim Down
3
@Tim,使用更短的符号真的有什么好处吗?虽然操作次数相同,但可读性较差。您对于此方法无法与期望具有假值类型的数组兼容也是正确的。就像所有工具一样,似乎只应在适当的情况下使用它。 - Justin Johnson
@Justin:我不认为缩短代码有任何好处,但我没有进行基准测试。这只是我在JS 1K比赛中经常使用的另一种形式 :) - Tim Down
1
@Juan:抱歉,我没注意到你在最后一段提到了假值。 - Tim Down
@Tim,前/后增量在创建错误方面很擅长。我从不在依赖于后/前减量行为的语句中使用它,以避免出现错误。 - Ruan Mendes
@Juan:好的,完全可以理解。我只有在尝试将我的代码压缩到1K以参加比赛之类的傻事时才会使用我提到的那种形式。 - Tim Down

3
首先,这有何难度或可读性不佳之处?
var i = someArray.length;
while(i--){
    //doStuff to someArray[i]
}

这不是什么奇怪的微优化,只是一种基本的工作避免原则。尽量不多次使用'.'或'[]'运算符应该像不重复计算π一样显而易见(假设你不知道我们已经在Math对象中有了它)。
如果someArray完全内部于函数,则其长度属性可以进行JIT优化,这实际上类似于一个getter,每次访问时都会计算数组的元素数量。JIT可以看到它完全在局部作用域中,并跳过实际的计数行为。
但这涉及到相当复杂的问题。每次对修改数组的任何操作,都必须将长度视为静态属性,并告诉您的数组修改方法(我指的是本机代码方面)手动设置该属性,而通常长度只是每次查询时都会计算项数。这意味着每次添加新的数组修改方法时,您都必须更新JIT以使局部作用域数组的长度引用分支行为。
我可以看到Chrome最终会做到这一点,但根据一些非常非正式的测试,我认为它还没有做到。我不确定IE是否会将此级别的性能微调作为重点。至于其他浏览器,您可以强烈主张维护问题,即必须为每个新数组方法分支行为,这比其价值更麻烦。至少,它不会得到高优先级。
最终,在典型的JS循环中,即使在旧浏览器中,每个循环周期访问长度属性也不会花费太多成本。但我建议养成缓存任何被执行超过一次的属性查找的习惯,因为对于getter属性,您永远无法确定正在做多少工作,哪些浏览器以什么方式进行了优化或者在将someArray移动到函数之外时可能遇到的性能成本,这可能导致每次执行该属性访问时,调用对象都要检查十几个地方才能找到它正在寻找的内容。
缓存属性查找和方法返回很容易,可以清除代码,并在修改面前使其更加灵活和性能稳健。即使有一两个JIT在涉及多个“if”的情况下使其无关紧要,您也不能确定它们总是会这样做,或者您的代码是否会继续使其成为可能。
因此,是的,抱歉反对让编译器处理的言论,但我不明白为什么您不想缓存属性。它很容易,它很简洁,它保证了更好的性能,而不管浏览器或对象的移动其属性被检查到外部作用域。
但是我真的很生气,因为现在Word文档的加载速度和1995年一样慢,而且人们继续编写可怕的低性能Java网站,尽管Java的VM据说在性能方面打败了所有非编译竞争者。我认为这种让编译器处理性能细节并且“现代计算机速度非常快”的观念与此有很大关系。在易于避免且不威胁可读性/可维护性的情况下,我们应该始终谨记避免工作,以我的看法。从长远来看,改变做事方式从未帮助我(或我怀疑任何人)更快地编写代码。

我在坦佩雷大学的C++老师说,while(i--)是最快的循环方式。而且它很简短。 - Timo Kähkönen
我可以稍微减少一些不高兴的情绪(并不是针对您,而是针对“编译器已经解决了这个问题”的现象),但我仍然坚持原则。 - Erik Reppen
@Domenic Ranty的一些位被移除,并用更深入的探讨替换了这个问题,即使所有浏览器JIT最终都优化了长度查找,不做长度查找仍然存在问题。 - Erik Reppen
2
Chrome已经进行了优化,mraleph在舞台上演示了它们是如何编译成相同的ASM代码的。这并不奇怪。 - Domenic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接