给定一个数组,.length
100
包含在各自索引处具有值0
到99
的元素,其中要求是找到数组的元素等于{ {1}}:n
。
为什么使用循环从数组的开始到结束迭代比从开始到结束和结束开始都要快?
51

const arr = Array.from({length: 100}, (_, i) => i);
const n = 51;
const len = arr.length;
console.time("iterate from start");
for (let i = 0; i < len; i++) {
if (arr[i] === n) break;
}
console.timeEnd("iterate from start");
&#13;
jsperf https://jsperf.com/iterate-from-start-iterate-from-start-and-end/1
答案 0 :(得分:39)
答案非常明显:
在判断代码速度时,您会看到它将执行多少操作。只需单步执行并计算它们。每条指令都需要一个或多个CPU周期,运行所需的时间越长。不同的指令占用不同的周期大多数并不重要 - 虽然数组查找可能比整数运算更昂贵,但它们基本上都需要恒定的时间,如果有太多,则它会占用算法的成本。
在您的示例中,您可能需要单独计算几种不同类型的操作:
(我们可能更精细,例如计算变量获取和存储操作,但那些几乎不重要 - 无论如何都在寄存器中 - 它们的数量基本上与其他数字成线性关系。)
现在你的两个代码迭代了大约50次 - 它们打破循环的元素位于数组的中间。忽略一些错误,这些都是重要的:
| forwards | forwards and backwards
---------------+------------+------------------------
>=/===/< | 100 | 200
++/-- | 50 | 100
a[b] | 50 | 100
&&/||/if/for | 100 | 200
考虑到这一点,并非出乎意料,做两倍的工作需要相当长的时间。
我还会回答你的评论中的几个问题:
第二个对象查找需要额外的时间吗?
是的,每个人的查询都很重要。它不像是可以立即执行,也不是优化为单个查找(如果它们查找相同的索引就可以想象)。
每个开始到结束和开始结束时是否应该有两个独立的循环?
操作次数无关紧要,只是为了他们的订单。
或者,换句话说,在数组中找到元素的最快方法是什么?
关于订单没有“最快”,如果您不知道元素的位置(并且它们均匀分布),您必须尝试每个索引。任何订单 - 甚至是随机订单 - 都会起作用。但请注意,您的代码严格更差,因为它在找不到元素时会查看每个索引两次 - 它不会在中间停止。
但是,微观优化这种循环还有一些不同的方法 - 检查these benchmarks。
let
(仍然?)慢于var
,请参阅Why is using `let` inside a `for` loop so slow on Chrome?和Why is let slower than var in a for loop in nodejs?。事实上,循环体范围的这种撕裂(大约50次)确实占据了运行时间 - 这就是为什么你的低效代码不是完全慢两倍的原因。0
的比较比与长度的比较略快,这使得循环向后有利。请参阅Why is iterating through an array backwards faster then forwards,JavaScript loop performance - Why is to decrement the iterator toward 0 faster than incrementing和Are loops really faster in reverse? 答案 1 :(得分:2)
@Bergi是对的。更多的操作是更多的时间。为什么?更多的CPU时钟周期。 时间实际上是指执行代码所需的时钟周期数。 为了得到你需要查看机器级代码(如汇编级代码)以找到真实证据的细节。每个CPU(内核?)时钟周期可以执行一条指令,因此您要执行多少条指令?
自从为嵌入式应用程序编写Motorola CPU以来,我没有计算很长时间的时钟周期。如果您的代码花费的时间更长,那么它实际上会产生更大的机器代码指令集,即使循环更短或运行次数相同。
永远不要忘记你的代码实际上被编译成一组CPU将要执行的命令(内存指针,指令代码级指针,中断等)。这就是计算机的工作方式,它在微控制器级别比ARM或摩托罗拉处理器更容易理解,但对于我们今天运行的复杂机器也是如此。
你的代码根本没有按你编写的方式运行(听起来很疯狂吗?)。它运行时编译为以机器级指令运行(编写编译器并不好玩)。数学表达式和逻辑可以编译成相当多的汇编,机器级代码,这取决于编译器选择如何解释它(它是位移等等,还记得二进制数学吗?)
参考: https://software.intel.com/en-us/articles/introduction-to-x64-assembly
你的问题很难回答,但是@Bergi说越多的操作时间越长,但为什么呢?执行代码所需的时钟周期越多。双核,四核,线程,汇编(机器语言)很复杂。但是没有代码会在您编写代码时执行。 C ++,C,Pascal,JavaScript,Java,除非你在汇编中编写(甚至编译成机器代码),但它更接近实际的执行代码。
CS中的主人,您将计算时钟周期和排序时间。您可能会在机器指令集上创建自己的语言。
大多数人都说谁在乎?内存今天很便宜,而且CPU正在快速尖叫并且变得更快。
但是有些关键应用需要10 ms,需要立即中断等等。
商业,美国宇航局,核电站,国防承包商,一些机器人,你明白了。 。 。我投票让它骑行并继续前进。
干杯, Wookie
答案 2 :(得分:1)
由于您要查找的元素总是大致位于数组的中间,因此您应该期望从数组的开头和结尾向内移动的版本大约需要两倍于从头开始。
每个变量更新都需要时间,每次比较都需要时间,而您需要花费两倍的时间。既然你知道在这个版本中需要一两次循环迭代才能终止,你应该认为它的成本大约是CPU时间的两倍。
此策略仍然是O(n)
时间复杂度,因为它只查看每个项目一次,当项目位于列表中心附近时,它会特别糟糕。如果它接近结束,这种方法将具有更好的预期运行时间。例如,尝试在两者中查找项目90。
答案 3 :(得分:1)
选择的答案非常好。我想添加另一个方面:尝试findIndex()
,它比使用循环快2-3倍:
const arr = Array.from({length: 900}, (_, i) => i);
const n = 51;
const len = arr.length;
console.time("iterate from start");
for (let i = 0; i < len; i++) {
if (arr[i] === n) break;
}
console.timeEnd("iterate from start");
console.time("iterate using findIndex");
var i = arr.findIndex(function(v) {
return v === n;
});
console.timeEnd("iterate using findIndex");
答案 4 :(得分:0)
这里的其他答案涵盖了主要原因,但我认为一个有趣的补充可能是提到缓存。
通常,顺序访问数组会更有效,特别是对于大型数组。当CPU从内存中读取数组时,它还会将附近的内存位置提取到缓存中。这意味着当您获取元素n
时,元素n+1
也可能被加载到缓存中。现在,缓存相对大,所以你的100 int数组可能很适合缓存。但是,在大得多的数组上,顺序读取比在数组的开始和结束之间切换要快。