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

Posted

技术标签:

【中文标题】为循环优化 JavaScript 真的有必要吗?【英文标题】:Is optimizing JavaScript for loops really necessary? 【发布时间】:2011-10-21 21:11:52 【问题描述】:

我读到not reading the length attribute of an array every iteration in the loop header 建议优化 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]);

但是,我created a small testcase 比较了这两种技术,但有时第一种情况更快,有时第二种情况。

您会推荐哪个版本?

【问题讨论】:

【参考方案1】:

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

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

A jsperf test designed for this question 给出了这个问题的定量结果。在这样的问题中,应该去 jsperf 看看真正的区别是什么,而不是过多的猜测。

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

现在,这种速度差异对您的脚本是否重要取决于具体的代码。如果您有一个经常循环的庞大数组,则在某些浏览器中使用此表单可能会产生有意义的差异:

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

在slightly different test case 中,当使用某些 DOM 函数返回的实时伪数组时,速度仍然存在差异,但没有被放大(我预计 DOM 伪实时数组的差异会更大,但事实并非如此)。

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

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

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

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

【讨论】:

我为您的 jsperf 添加了第二个修订版,并进行了更多测试...jsperf.com/loop-iteration-length-comparison-variations/2(只是为了满足我自己的兴趣!) @Matt - 我原以为您的版本会更快,但事实并非如此(在 Chrome 或 IE9 中)。我想知道为什么? 我也是这么想的。事实上,我可以发誓我在某处读到它是……因此我的好奇心:P。在 IE9 中,我发现运行非常相似,而在 Chrome 中,for 循环的性能大大优于 while 循环......我只能在这里假设 Chrome 优化了 for 循环。【参考方案2】:

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

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

【讨论】:

在我的回答中的 jspref 测试中仍然显示 IE9 的差异高达 50%。有时即使这也不相关,但有时它会(迭代非常大的数组或进行大量重复迭代或在速度非常关键的事情中,等等......)。我不会在所有情况下都忽略它。 什么的 50%?你在循环中有什么真实发生的事情吗?我仍然声称这种差异对实际的 Javascript 程序很重要的可能性很小。 您可以在我的回答中查看我的 jsperf 以了解它的具体测量值。这就是定量测试有用的原因。没有猜想,这一切都在测试中说明了。我的观点是,在某些情况下,性能差异是相关的,因此不应将其视为总是不相关的。强大的程序员应该接受培训,以识别何时为提高性能而做一些额外的工作是值得的,何时不值得。循环的性能在 JS 中并不总是无关紧要的。 我说要考虑您的要求。然后,实施满足您要求的最简单(正确且可读)的解决方案。有时良好的最佳循环性能是一项重要要求。在这些情况下,预先缓存长度可能很有用。毕竟,我们只是在讨论 是的,循环体几乎是空的。如果您尝试测量测试中的特定方面,您通常希望尽可能地隔离该问题。这种循环差异在您的应用程序中的相关程度取决于循环的其余部分所花费的时间。原始循环时间可能是您的大部分执行时间,也可能几乎没有,这取决于循环中发生了什么。【参考方案3】:

我会推荐第二个:

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

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

【讨论】:

请参阅下面我的答案中的 jsperf 测试。在某些浏览器中,这并不是一个荒谬的微优化(IE9 的速度差异可能高达 50%),因此它取决于确切的上下文。我不会认为它永远不相关。这取决于正在执行的操作与速度差异的相关程度。 “因为它更简洁,更惯用” - 这只是一个偏好问题......而且您将偏好置于效率之上 @gion_13 编写可读代码时,是的。这类事情是 YAGNI 的一部分:这是一种不必要的优化,不太可能成为代码中的瓶颈,而且阅读起来更加乏味。如果您正在编写一些要求您的代码针对性能进行优化的东西,您可以稍后进行更改。 我真的不明白如何将声明与逻辑分开可以被视为“瓶颈”。对不起,我不是要争论,我们显然有不同的意见。【参考方案4】:

作为一般规则,缓存循环的“停止值”(在您的情况下为 names.length)只有在它是计算值时才有价值。对于有问题的数组,它只是一个查找,所以缓存它不会有什么好处。

【讨论】:

【参考方案5】:

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

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

【讨论】:

哇,您的测试用例确实显示出巨大的差异。因此,当使用 .childElementCount 或类似的方式访问 DOM 时,优化后的版本肯定会更好。【参考方案6】:

我会推荐

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

【讨论】:

@NedBatchelder:哎呀。谢谢你:) 更清晰,IMO。【参考方案7】:

2017 年更新答案

您应该使用优化/最佳实践方式。

在您的确切示例中:它是如此微不足道,以至于无关紧要。正如@jfriend00 所说,即使有 50% 的性能差异,也没有多大意义。 CPU(包括当前的智能手机)每秒可以进行数百万次计算。这意味着毫秒甚至不会向用户注册,这与@Ned Batchelder 发布的内容一致。

但是,编码不应该是关于你可以逃避的事情。也就是说,正如@DwB 所说,“......停止价值......只有当它是计算值时才有价值。”考虑到这一点,以下代码给出了一个返回停止值的浪费时间函数的示例。在这里,速度有多么不同就变得很明显了。将服务器、复杂的客户端代码和其他密集计算的潜在缺点相乘,您将通过使用最佳实践来改善用户体验。

		var eCount = document.getElementById("loopCount");
		var waitDiv = document.getElementById("waitDiv");
		var runButton = document.getElementById("runButton");
		var interCount = eCount.value.replace(/\D/g,'');
		var names = ['George','Ringo','Paul','John'];
		
		eCount.addEventListener("input", function()			
			var value = parseInt(this.value.replace(/\D/g,'')).toLocaleString();
			this.value = value.toLocaleString();
		);


		function runLoop()			
			interCount = eCount.value.replace(/\D/g,'');
			waitImg(true);
			setTimeout(function()
				var cachedTime = loopTest("cached");
				var inlineTime = loopTest("inline");
				document.getElementById( "cached" ).innerText = cachedTime+" Cached";
				document.getElementById( "inline" ).innerText = inlineTime+" Not Cached";
				waitImg(false);
			, 100); // delay to allow update of DOM with waitimg gif
			
		
		
		function loopTest(meth)
			var worthlessVariable = 0;
			var t0 = performance.now();			
			if( meth == "cached" )
				for( var i = 0, len = busyCalulations(); i < len; i++) 
					worthlessVariable = i;
				
			else
				for( var i = 0; i < busyCalulations(); i++) 
					worthlessVariable = i;
				
			
			var t1 = performance.now();
			return (t1 - t0);
		

		
		function busyCalulations()
			// garbage math to simulate doing something
			// it returns interCount after some pointless math
			var limit = Math.floor(Math.random() * 20) + 20;
			return interCount*(limit*names.length)/(limit*names.length);
		
		
		
		function waitImg(txt) // display wait timer
			if (txt === true)
				waitDiv.style.visibility = "visible";
				runButton.style.visibility = "hidden";
			else
				waitDiv.style.visibility = "hidden";
				runButton.style.visibility = "visible";
			
		
	<h1>Loop Tester</h1>
<form onSubmit="return false;">
	Loop Length <input id="loopCount" type="text" value="100,000"><br>
	<br><br>
	<button id="runButton" onClick="runLoop();">Run Test</button>
	<div id="waitDiv" style="visibility: hidden"><img src="https://i.stack.imgur.com/5qXc3.gif"></div>
	<br><br>
	</form>
	<div><p>Times are in milliseconds</p>
		<div id="cached"></div>
		<div id="inline"></div>
	</div>

【讨论】:

【参考方案8】:

请参考this 的帖子,里面谈到了优化 JS 循环。

所以一个简单的解决方案是:

let arr = ["a", "b", "c", "d", "e"]
let i = arr.length
while(i--) 
   callFn();

与其他传统循环技术(如

)相比,上述代码运行速度更快
for(let i = 0; i < arr.length; i++) 

因为上面的代码必须在每次迭代时获取arr.length

【讨论】:

以上是关于为循环优化 JavaScript 真的有必要吗?的主要内容,如果未能解决你的问题,请参考以下文章

如果不使用 Ajax,客户端验证真的有必要吗?

你真的会写for循环吗?来看看这些常见的for循环优化方式

你真的会写for循环吗?来看看这些常见的for循环优化方式

真的有必要用rxjava吗

javascript排序算法整理

引用 url() 的值真的有必要吗?