真的有必要为循环优化 JavaScript 吗?

IT技术 javascript performance optimization loops for-loop
2021-03-15 06:53:48

我读到建议通过在循环头中每次迭代时读取数组的长度属性来优化 JavaScript中的循环

所以,我们应该这样做:

var names = ['George','Ringo','Paul','John'];
for(var i=0,j=names.length;i<j;i++){// Read array length once and assign it to a variable
    doSomeThingWith(names[i]);
}

而不是这个:

var names = ['George','Ringo','Paul','John'];
for(var i=0;i<names.length;i++){
    doSomeThingWith(names[i]);
}

但是,我创建了一个小测试用例来比较这两种技术,但有时第一种情况更快,有时第二种情况更快。

你会推荐哪个版本?

6个回答

首先,我应该说这个答案是在 2011 年写的,这些东西随着时间的推移而改变(随着浏览器解释器优化越来越多的东西)所以如果你真的想知道世界的当前状态,你必须在当前浏览器上运行测试.

在任何版本的 IE 上运行您自己的 jsperf 测试在那里您会看到这两种方法或许多其他旧浏览器之间的一致差异。你显然只在 Chrome 上运行它,它是如此之快,如此优化,以至于这两种方法之间的差异可以忽略不计。在 IE9 上(可能比 IE7 和 IE8 好得多),预缓存长度的方法快了 31%。

为这个问题设计的 jsperf 测试给出了这个问题的定量结果。在像这样的问题中,应该去 jsperf 看看真正的区别是什么,而不是那么多的猜测。

它显示了我尝试过的浏览器的差异,从几乎没有差异到相当大的差异取决于浏览器。 在 Chrome 中,几乎没有区别。在 IE9 中,首先存储长度几乎快了 50%。

现在,这种速度差异对您的脚本是否重要取决于特定的代码。如果您有一个经常循环的巨大数组,那么在某些浏览器中使用这种形式可能会产生有意义的不同:

for (var i = 0, len = list.length; i < len; i++) {
    // do code here
} 

使用某些 DOM 函数返回的实时伪数组时,稍微不同的测试用例中,速度仍然存在差异,但没有放大(我预计 DOM 伪实时数组的差异会更大,但事实并非如此)。

在实践中,当我认为我的代码部分对速度不敏感和/或数组不大时,我倾向于使用短版本(较少输入),如果我认为我会使用预先缓存长度的较长版本我有意识地考虑速度或数组很大,或者我正在对同一个数组进行大量迭代。

预缓存长度还有其他几个编程原因。如果您将在循环期间将元素添加到数组的末尾,并且您不希望循环遍历那些新添加的元素,那么您将需要预加载长度并且只遍历最初存在的元素.

for (var i = 0, len = list.length; i < len; i++) {
    if (list[i] == "whatever") {
        list.push("something");
    }
} 

请记住,浏览器在不断发展并添加越来越多的优化,因此在 2011 年显示出巨大收益的优化可能在未来基本上内置到更现代的浏览器中,因此不再需要手动编码优化。因此,如果您试图针对当今的性能优化某些内容,则必须在当今的浏览器中进行测试,您不能仅仅依赖于您阅读的可能已有几年历史的内容。

@Matt - 我原以为你的版本会更快,但它们不是(在 Chrome 或 IE9 中)。我想知道为什么?
2021-04-23 06:53:48
我通过几个测试为您的 jsperf 添加了第二个修订版... jsperf.com/loop-iteration-length-comparison-variations/2(只是为了满足我自己的兴趣!)
2021-05-17 06:53:48
我也是这么想的。事实上,我可以发誓我在某处读到它是......因此我的好奇心:P。在 IE9 中,我发现运行非常相似,而在 Chrome 中,for循环的性能大大优于while循环......我只能假设 Chrome 为for循环进行了优化
2021-05-19 06:53:48

这个建议充其量只是一个微优化,并且所有的工作都是在 Javascript 引擎的速度上完成的,它不太可能再成为可衡量的差异。也许在一个很长很紧的循环中的某个地方可能会有所作为,但我对此表示怀疑。

出于某种原因,程序员倾向于将速度放在首位,即使这是毫无根据的。先考虑正确性,然后再考虑可读性。

我说考虑一下你的要求。然后,实施满足您要求的最简单(正确且可读)的解决方案。有时良好的最佳循环性能是一个重要的要求。在这些情况下,预先缓存长度可能很有用。毕竟,我们只是在谈论 <10 个以上的代码字符,而没有额外的代码行。
2021-04-25 06:53:48
您可以在我的回答中查看我的 jsperf 以准确了解它的测量内容。这就是定量测试有用的原因。没有猜想,一切都在测试中说明了。我的观点是,在某些情况下,性能差异是相关的,因此不应将其视为总是无关紧要的。应该训练强大的程序员来识别什么时候值得做一些额外的工作来提高性能,什么时候不值得。循环的性能在 JS 中并不总是无关紧要的。
2021-05-03 06:53:48
在我的回答中的 jspref 测试中测得的 IE9 中仍然显示出高达 50% 的差异。有时甚至这都不相关,但有时它会是(迭代非常大的数组或进行大量重复迭代或对速度非常关键的东西,等等......)。我不会在所有情况下都忽略它。
2021-05-07 06:53:48
是的,循环体几乎是空的。如果您试图衡量测试中的特定方面,您通常希望尽可能地隔离该问题。这种循环差异在您的应用程序中的相关程度取决于循环的其余部分所花费的时间。原始循环时间可能是您的大部分执行时间,也可能几乎没有,这取决于循环中发生的事情。
2021-05-16 06:53:48
50%什么?你有什么真正发生在循环中吗?我仍然声称,这种差异对实际的 Javascript 程序根本没有影响的可能性很小。
2021-05-19 06:53:48

我会推荐第二个:

var names = ['George','Ringo','Paul','John'];
for (var i = 0; i < names.length; i++) {
  doSomeThingWith(names[i]);
}

因为它更简洁,更地道。除非您正在进行一些荒谬的微优化,否则您永远不需要使用第一个。

我真的不明白如何将声明与逻辑分开可以被视为“瓶颈”。对不起,我不是要争论,我们显然有不同的意见。
2021-05-01 06:53:48
@gion_13 在编写可读代码时,是的。这类事情是 YAGNI 的一部分:它是一种不必要的优化,不太可能成为您代码中的瓶颈,而且会使阅读变得更烦人。如果您正在编写一些要求您的代码针对性能进行优化的内容,您可以稍后更改它。
2021-05-08 06:53:48
请参阅下面我的回答中的 jsperf 测试。在某些浏览器中,这并不是一个荒谬的微优化(IE9 的速度差异可能高达 50%),因此它取决于确切的上下文。我不会认为它永远不相关。这取决于正在执行的操作与速度差异的相关程度。
2021-05-10 06:53:48
“因为它更简洁、更地道”——这只是一个偏好问题……你把偏好放在效率之上
2021-05-11 06:53:48

作为一般规则,缓存循环的“停止值”(在你的情况下,names.length)只有在它是一个计算值时才有value。对于有问题的数组,它只是一个查找,因此通过缓存它您将获得的收益很小。

定义"really necessary"
如果您遍历 4 个元素的数组,我认为即使 IE 也不介意,但请记住,您可能必须遍历一些 dom 元素;假设您有一个ul包含 1.000.000(或更多)个入口 ( li)的列表( )。我认为声明一个额外的变量可以让你检查 ul 的长度属性一百万次。
也许我对百万部分夸大了一点,但看看只有 10000测试结果li
优化后的循环几乎比“正常”循环快一百倍。

我的结论是:优化你的循环......它不会伤害你(或你的代码或你的浏览器)。

哇,您的测试用例确实显示出巨大的差异。因此,当使用 .childElementCount 或类似方法访问 DOM 时,优化版本肯定更好。
2021-04-27 06:53:48