For 循环性能:将数组长度存储在变量中

IT技术 javascript performance for-loop
2021-01-14 08:01:43

考虑相同循环迭代的两个版本:

for (var i = 0; i < nodes.length; i++) {
    ...
}

var len = nodes.length;
for (var i = 0; i < len; i++) {
    ...
}

无论如何,后一个版本比前一个版本快吗?

6个回答

接受的答案是不正确的,因为任何体面的引擎都应该能够使用如此简单的循环体将属性负载提升到循环之外。

这个 jsperf - 至少在 V8 中,有趣的是看到实际将它存储在变量中如何改变寄存器分配 - 在使用sum变量的代码中,变量存储在堆栈中,而使用array.length-in-a-loop-code它存储在寄存器中。我认为在 SpiderMonkey 和 Opera 中也发生了类似的事情。

据作者介绍,JSPerf 被错误地使用,有 70% 的时间。此处所有答案中给出的这些损坏的 jsperfs 给出了误导性的结果,人们从中得出了错误的结论。

一些危险信号是将代码放在测试用例中而不是函数中,而不是测试结果的正确性或使用某种消除死代码的机制,在设置或测试用例中定义函数而不是全局......为了一致性,你需要预热 -在任何基准测试之前也要启动测试函数,这样编译就不会发生在计时部分。

好吧,只要运行接受的答案,我的 Chromium 88 的缓存长度几乎快两倍
2021-03-19 08:01:43
很多人忘记了编译器的用途,并且没有必要为了纳米级的收益而玩弄代码,从长远来看甚至可能是有害的。
2021-03-21 08:01:43
有趣的是,使用接受的答案的 jsperf 和 100000 次迭代,它说(对我来说)首先设置长度要慢 33%(可靠)。如果我在定义之后的准备工作中添加了对 worker() 的调用,结果就会翻转,将循环外的长度设置为快 33%。
2021-03-21 08:01:43
不是我的。我也讨厌那些反对票。好吧,你的回答看起来很有趣,我只是需要更多时间来深入研究这个话题。谢谢你。
2021-03-30 08:01:43

更新:16/12/2015

由于这个答案似乎仍然获得了很多观点,我想随着浏览器和 JS 引擎的不断发展重新审视这个问题。

我没有使用 JSPerf,而是将一些代码放在一起,使用原始问题中提到的两种方法循环遍历数组。我已经将代码放入函数中以分解功能,就像在现实世界的应用程序中希望的那样:

function getTestArray(numEntries) {
  var testArray = [];
  for (var i = 0; i < numEntries; i++) {
    testArray.push(Math.random());
  }
  return testArray;
}

function testInVariable(testArray) {
  for (var i = 0; i < testArray.length; i++) {
    doSomethingAwesome(testArray[i]);
  }
}

function testInLoop(testArray) {
  var len = testArray.length;
  for (var i = 0; i < len; i++) {
    doSomethingAwesome(testArray[i]);
  }
}

function doSomethingAwesome(i) {
  return i + 2;
}

function runAndAverageTest(testToRun, testArray, numTimesToRun) {
  var totalTime = 0;
  for (var i = 0; i < numTimesToRun; i++) {
    var start = new Date();
    testToRun(testArray);
    var end = new Date();
    totalTime += (end - start);
  }
  return totalTime / numTimesToRun;
}

function runTests() {
  var smallTestArray = getTestArray(10000);
  var largeTestArray = getTestArray(10000000);

  var smallTestInLoop = runAndAverageTest(testInLoop, smallTestArray, 5);
  var largeTestInLoop = runAndAverageTest(testInLoop, largeTestArray, 5);
  var smallTestVariable = runAndAverageTest(testInVariable, smallTestArray, 5);
  var largeTestVariable = runAndAverageTest(testInVariable, largeTestArray, 5);

  console.log("Length in for statement (small array): " + smallTestInLoop + "ms");
  console.log("Length in for statement (large array): " + largeTestInLoop + "ms");
  console.log("Length in variable (small array): " + smallTestVariable + "ms");
  console.log("Length in variable (large array): " + largeTestVariable + "ms");
}

console.log("Iteration 1");
runTests();
console.log("Iteration 2");
runTests();
console.log("Iteration 3");
runTests();

为了实现尽可能公平的测试,每个测试运行 5 次并取平均值。我还运行了整个测试,包括生成数组 3 次。在我的机器上对 Chrome 进行的测试表明,使用每种方法所花费的时间几乎相同。

重要的是要记住,这个示例有点像玩具示例,实际上,从应用程序上下文中取出的大多数示例可能会产生不可靠的信息,因为您的代码正在执行的其他操作可能会直接或间接地影响性能。

底线

确定什么最适合您的应用程序的最佳方法是自己测试它!JS 引擎、浏览器技术和 CPU 技术在不断发展,因此您必须始终在应用程序的上下文中为自己测试性能。还值得问问自己是否有性能问题,如果没有,那么花时间进行用户无法察觉的微优化可能会更好地用于修复错误和添加功能,从而使用户更快乐:)。

原答案:

后者会稍微快一点。length属性不会遍历数组来检查元素的数量,但每次在数组上调用它时,都必须取消对该数组的引用。通过将长度存储在变量中,循环的每次迭代都不需要取消引用数组。

如果您对在 javascript 中循环遍历数组的不同方式的性能感兴趣,请查看此jsperf

length 是一个属性,而不是一个函数。
2021-03-23 08:01:43
你的回答是最准确的。我认为差异并不显着,特别是考虑到铬。
2021-03-31 08:01:43
有趣的是,如果runTests在循环测试之前计算缓存变量测试,则缓存长度的时间量减少两倍。但是, ifrunTests将首先在循环测试中计算,然后缓存。时间将非常相似。在我的 Chromium 88 中。有什么想法吗?
2021-04-06 08:01:43
我认为您已经颠倒了测试功能的名称。testInLoop实际上是将长度缓存在变量中的那个。
2021-04-08 08:01:43
根据其 jsperf,这个答案是错误的,或者至少不再正确。不过,意图是正确的。引用的 jsperf 使用了无关紧要的循环大小。对于 1000 次迭代,几乎没有区别。将循环提高到 100000 表示先设置长度(而不是循环中的 arr.length)实际上慢了 33%,这也是错误的。请参阅@Esilija 的回答。
2021-04-09 08:01:43

根据w3schools “减少循环中的活动”,以下被认为是错误的代码:

for (i = 0; i < arr.length; i++) {

以下被认为是好的代码:

var arrLength = arr.length;
for (i = 0; i < arrLength; i++) {

由于访问 DOM 很慢,因此编写了以下内容来测试该理论:

<!doctype html>

<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>my test scripts</title>
	</head>
	
	<body>
		<button onclick="initArray()">Init Large Array</button>
		<button onclick="iterateArraySlowly()">Iterate Large Array Slowly</button>
		<button onclick="iterateArrayQuickly()">Iterate Large Array Quickly</button>
		
		<p id="slow">Slow Time: </p>
		<p id="fast">Fast Time: </p>
		<p id="access"></p>


	
	<script>
	var myArray = [];
			
		function initArray(){
			var length = 1e6;
			var i;
			for(i = 0; i < length; i++) {
				myArray[i] = i;
			}
			console.log("array size: " + myArray.length);
		}
		
		function iterateArraySlowly() {
			var t0 = new Date().getTime();
			var slowText = "Slow Time: "
			var i, t;
			var elm = document.getElementById("slow");
			for (i = 0; i < myArray.length; i++) {
				document.getElementById("access").innerHTML = "Value: " + i;
			}
			t = new Date().getTime() - t0;
			elm.innerHTML = slowText + t + "ms";
		}
		
		function iterateArrayQuickly() {
			var t0 = new Date().getTime();
			var fastText = "Fast Time: "
			var i, t;
			var elm = document.getElementById("fast");
			var length = myArray.length;
			for (i = 0; i < length; i++) {
				document.getElementById("access").innerHTML = "Value: " + i;
			}
			t = new Date().getTime() - t0;
			elm.innerHTML = fastText + t + "ms";
		
		}
	</script>
	</body>
</html>

有趣的是,首先执行的迭代似乎总是胜过另一个。但是在大多数情况下,被认为是“坏代码”的代码似乎在每个代码都执行了几次之后获胜。也许比我更聪明的人可以解释原因。但就目前而言,在语法上,我坚持对我来说更易读的内容:

for (i = 0; i < arr.length; i++) {

如果nodes是,DOM nodeList那么第二个循环会快得多,因为在第一个循环中,您在每次迭代时查找 DOM(非常昂贵)。jsperf

我想这取决于它是否是一个实时的NodeList。静态 NodeList 不应该有任何额外的开销。
2021-03-19 08:01:43
有关实时与静态 NodeList 对象的详细信息,请访问:developer.mozilla.org/en-US/docs/Web/API/NodeList
2021-03-21 08:01:43

在我使用过的任何基准测试中,这一直是性能最高的。

for (i = 0, val; val = nodes[i]; i++) {
    doSomethingAwesome(val);
}