在研究阵列分配性能时,我偶然发现了jsperf,据称这种形式的for
循环的速度显着提高:
var i, j = 0;
for (i = 0; i < n; i++) {
myArray[j++] = i;
}
...在当前的浏览器中。这种“双增量”形式对我来说完全不同,而且看起来很奇怪。我找不到任何关于它的讨论。
多年来,由于编译器优化越来越聪明,Many people警告微型计算机误导数据。这是其中一个案例吗?为什么或者为什么不?如果jsperf写得不正确,我很乐意看到更正确,更准确地反映现实世界条件的正确修订的jsperf。
答案 0 :(得分:4)
除非JS正在做一些非常差/奇怪的优化,否则这个双计数器循环与单个计数器循环相比应该没有办法更快。从机器级别的角度来看,这样的代码通常使用两个通用寄存器而不是一个,必须增加两者。差异应该非常小并且通常无关紧要,但单个计数器应该具有轻微的性能优势。
确定的唯一方法是查看生成的机器说明/反汇编。
使用length
并不像访问变量一样简单。如果是这种情况,对我来说有点意外,但它可能转化为更多指令(例如:涉及最坏情况下的分支)。但在这种情况下,只使用i
代替j
并仍然是单计数器循环,结果仍然会稍快一些。
值得一试的是改变测试的顺序。以前与分页/缓存相关的测试中存在的内存可能正在发生,这使后者的双计数器测试更快。例如,双重计数器测试紧接着是单个计数器测试。尝试按照他们执行的顺序交换这两个,看看它是否会影响结果。
如评论中的Mark测试所示,push-numbers-redux,在避免length
时,单个计数器循环确实可以获得更快的结果。显然length
确实需要比简单变量更多的指令,可能对优化器无法消除的关联数组情况进行一些分支。但是,如果我们针对变量测试常规变量,单个计数器循环仍将击败双重计数器。
由于这个主题也被提出为什么微观基准可能有点坏,他们并不总是坏的,但有一些警告与他们相关。我会说你的测试非常微观,因为它从用户的角度来看没有做任何有意义的事情。它创建一个数据数组,但不对它做任何事情。
如果您尝试从这些测试中概括性能创意,那么为什么这会很糟糕,首先,硬件是一个动态机器。它试图预测你在做什么,更常见的代码分支,将DRAM转移到缓存等。操作系统也是动态的,即时分页内存。考虑到环境的动态特性,您可能会面临编写微测试的危险,这种测试看起来更快,但只是对这些动态因素感到幸运。真实世界的测试做了更多的工作,也许更重要的是,各种各样的工作,往往会减轻这种动态的运气&#34;可能会误导你相信某些东西的因素通常会更快,因为它可能只会对你的特定测试更快。你不必编写成熟的大型应用程序来获得真实世界的东西。例如,计算Mandelbrot集是一个相当简单的代码(可以放在一个页面中),但仍然足以逃脱这些微观层面的危险。
另一个危险是优化编译器。当优化器检测到您基本上没有引起任何全局副作用时,您可能会陷入微基准测试(例如:计算数据只是为了丢弃它而不打印它或对其进行任何操作以引起其他地方的更改)。使用非常复杂的优化器,他们可以检测何时可以跳过某些计算,因为它们不会产生任何副作用,并且您有时可能会发现您所做的事情导致事情运行速度提高了10,000倍,但并不是因为实际工作的速度更快,而是因为优化器认为它根本不需要完成并且完全跳过它。如果,在您的测试中,您将自己置于用户的角度,并且可以合理化为什么可以跳过代码的某些部分并仍然为用户提供相同的输出/结果而无需等待很长时间,那么优化器也可以跳过那段代码。每当你针对一个强大的优化器进行微基准测试并找到一个看起来好得令人难以置信的结果时,它可能真的太好了,而且优化器只是直接跳过工作,因为它在你的表面测试中注意到了它实际上并不需要这样做。
最后但并非最不重要的是,专注于微观效率通常会让你进入类似集合的思维。当您在紧密循环中反复测试一些指令时,没有任何摆动空间可以以更智能的方式进行优化。通常,在盯着性能测量时,您需要更多的摆动空间,从粗略优化(算法,多线程等)到最小的微优化。如果您的测试是微观的,那么您最终会立即采用最细粒度的金属刮削思维方式,当现实世界可能为您提供节省数十亿美元的机会时,这会导致对保存几个时钟周期的不健康的痴迷投入相同的努力/时间。