【问题标题】:Performance gain from double counter in a js "for" loop?js“for”循环中双计数器的性能提升?
【发布时间】:2015-06-03 23:14:10
【问题描述】:

在研究数组分配性能时,我偶然发现了这个jsperf,据称它显示for 形式的循环的速度非常显着:

var i, j = 0;
for (i = 0; i < n; i++) {
  myArray[j++] = i;
}

... 在当前浏览器中。这种“双增量”形式对我来说完全陌生,而且看起来很奇怪。我找不到任何关于它的讨论。

Many people 多年来一直警告说,由于编译器优化越来越聪明,来自微基准的误导性数据。这是其中一种情况吗?为什么或者为什么不?如果 jsperf 写错了,我希望看到一个正确的、经过修订的 jsperf,它更准确地反映了现实世界的情况。

【问题讨论】:

  • 我真的不知道你在问什么。你是在问为什么使用myArray[j++] = i 而不是myArray[i] = i?如果是这样,请在您的问题中说出来。
  • 是的,那些测试用例是有缺陷的。每个循环前面都应该有 `myArray = [];, otherwise they measure totally different things depending on how the global myArray` 被修改。
  • @Bergi 这不是问题所在。 “准备”代码在每次测试之前运行。
  • @Mark:不,它没有。它只在每个测试循环之前运行一次。
  • @Bergi 我没有关注。测试循环。它应该在每个测试循环之前运行一次。有什么问题?

标签: javascript arrays performance benchmarking


【解决方案1】:

除非 JS 在优化方面做得非常糟糕/奇怪,否则这种双计数器循环与单计数器循环应该不可能更快。从机器级别的角度来看,这样的代码通常会使用两个通用寄存器而不是一个,必须同时递增两者。差异应该非常小并且通常可以忽略不计,但单个计数器应该具有轻微的性能优势。

唯一确定的方法是查看生成的机器指令/反汇编。

使用length 可能并不像访问变量那么简单。如果是这种情况,我有点惊讶,但它可能会转化为更多指令(例如:在最坏的情况下涉及分支)。但在这种情况下,您仍然应该只使用 i 而不是 j 获得稍快的结果,并且仍然是单计数器循环。

值得尝试的一件事是改变测试的顺序。以前的与分页/缓存相关的测试中的内存可能会发生一些事情,这使得后者的双计数器测试运行得更快。例如,在双计数器测试之后立即进行单计数器测试。尝试按照它们的执行顺序交换这两个,看看它是否会影响结果。

更新

正如 Mark 在 cmets 中的测试 push-numbers-redux 所示,在避免 length 时,他确实使用单个计数器循环获得了更快的结果。显然,length 确实需要比简单变量更多的指令,也许对于优化器无法消除的关联数组情况有一些分支。但是,如果我们针对变量测试常规变量,单计数器循环仍然会胜过双计数器循环。

微基准

由于还提出了这个主题,即为什么微基准可能会有些糟糕,它们并不总是很糟糕,但有一些与它们相关的警告。我会说你的测试是非常微小的,因为从用户的角度来看它没有做任何有意义的事情。它创建了一个数据数组,只是对它不做任何事情。

如果您试图从此类测试中概括性能想法,为什么这可能会很糟糕,首先,硬件是一台动态机器。它试图预测你在做什么,哪些代码分支将更常被执行,将 DRAM 传输到缓存等。操作系统也是动态的,动态分页内存。鉴于环境的动态特性,您可能会面临编写看起来更快但只是幸运地利用这些动态因素的微测试的危险。做更多工作的真实世界测试,也许更重要的是,各种各样的工作,往往会减轻“动态运气”因素,这种因素可能会误导你相信某些东西通常更快,而它可能只对你的特定测试更快。您不必编写成熟的大型应用程序即可获得类似现实世界的东西。例如,计算 Mandelbrot 集是相当简单的代码(可以放在一页中),但仍然足以避免那些微观层面的危险。

另一个危险是优化编译器。当优化器检测到您基本上没有引起全局副作用时,您可能会陷入微基准测试(例如:计算数据只是为了丢弃它而不打印它或对它做任何事情以导致其他地方发生变化)。使用非常复杂的优化器,它们可以检测到何时可以跳过某些计算,因为它们不会产生副作用,并且您有时可能会发现您所做的某些事情会使运行速度提高 10,000 倍,但这并不是因为实际工作完成得更快,而是因为优化器认为它根本不需要完成并直接跳过它。如果在您的测试中,您设身处地为用户着想,并且可以合理化为什么可以跳过代码的某些部分,并且仍然为用户提供相同的输出/结果而无需等待很长时间,那么优化器也可以跳过那个代码。每当您针对强大的优化器进行微基准测试并发现似乎好得令人难以置信的结果时,它可能好得令人难以置信,并且优化器只是直接跳过工作,因为它在您的表面测试中注意到它实际上不需要这样做。

最后但并非最不重要的一点是,专注于微观效率通常会让您陷入那种组装式思维。当您只是在一个紧密的循环中重复测试一些指令时,就没有以更智能的方式进行优化的回旋余地。通常,在进行性能测量时,您需要更多的回旋余地,从粗略的优化(算法、多线程等)到最小的微优化。当您的测试是微观的时,您最终会立即进入最精细的金属刮削思维,这可能会导致对节省几个时钟周期的不健康的痴迷,而现实世界可能会为您提供节省数十亿美元的机会投入相同的精力/时间。

【讨论】:

  • “使用长度也可能不像访问变量那么简单。” - 确实。它使世界变得与众不同。 i 更快。 jsperf.com/push-numbers-redux
  • 啊,这很有意义。我最初写答案时认为问题是关于双计数器循环与单计数器循环,然后意识到性能测试是使用lengthj。您的结果对我来说非常有意义,而且似乎length 可能涉及分支(也许是为了检测优化器在仅用作基本数组时未优化的关联数组情况)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-21
  • 2013-07-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-08
  • 1970-01-01
相关资源
最近更新 更多