js "for" 循环中双计数器的性能提升?
Performance gain from double counter in a js "for" loop?
在研究数组分配性能时,我偶然发现了这个 jsperf,据称它显示了 for
形式循环的非常显着的速度提升:
var i, j = 0;
for (i = 0; i < n; i++) {
myArray[j++] = i;
}
...在当前的浏览器中。这种 "double increment" 形式对我来说完全陌生,看起来很奇怪。我找不到任何关于它的讨论。
Many people 多年来一直警告说,由于编译器优化的越来越聪明,来自微基准测试的误导性数据。这是其中之一吗?为什么或者为什么不?如果 jsperf 写错了,我很乐意看到一个正确的、修改后的 jsperf,它能更准确地反映现实世界的情况。
除非 JS 正在做一些非常 poor/bizarre 的优化,否则这种双计数器循环与单计数器循环相比应该没有更快的方法。从机器级别的角度来看,这样的代码通常会使用两个通用寄存器而不是一个,必须递增两个。差异应该很小,一般来说可以忽略不计,但单个计数器应该有轻微的性能优势。
唯一确定的方法是查看生成的机器 instructions/disassembly。
使用 length
可能不像访问变量那么简单。如果是这样的话,我有点惊讶,但它可能会转化为更多的指令(例如:在最坏的情况下涉及分支)。但是在那种情况下,您仍然应该使用 i
而不是 j
并仍然是一个单计数器循环来获得稍微更快的结果。
值得尝试的一件事是改变测试的顺序。之前与 paging/caching 相关的测试的内存可能发生了一些变化,这使得后面的双计数器测试进行得更快。例如,双计数器测试后立即进行单计数器测试。尝试按执行顺序交换这两个,看看它是否会影响结果。
更新
如评论中 Mark 的测试所示,push-numbers-redux,他在避免 length
时使用单个计数器循环确实获得了更快的结果。显然 length
确实需要比简单变量更多的指令,也许对于优化器无法消除的关联数组情况有一些分支。但是,如果我们正在针对变量测试常规变量,那么单计数器循环仍将击败双计数器循环。
微基准测试
由于还提出了关于为什么微基准测试可能有些糟糕的主题,因此它们并不总是很糟糕,但有一些与之相关的警告。我会说您的测试非常微观,因为从用户的角度来看它没有做任何有意义的事情。它创建了一个数据数组,但什么都不做。
如果您试图从此类测试中概括性能想法,为什么这会很糟糕,首先,硬件是一台动态机器。它试图预测你在做什么,哪些代码分支会更常执行,将 DRAM 传输到缓存等。操作系统也是动态的,动态分页内存。考虑到环境的动态特性,您可能会面临编写微型测试的危险,该测试看起来速度更快,但只是对这些动态因素感到幸运。做更多工作的真实世界测试,也许更重要的是,各种各样的工作,往往会减轻 "dynamic luck" 可能误导你相信某些东西通常更快的因素,而实际上它可能只对你的特定测试更快。您不必编写成熟的大型应用程序来获得类似真实世界的东西。例如,计算 Mandelbrot 集是相当简单的代码(可以放在一页中),但仍然足以避免那些微观层面的危险。
另一个危险是优化编译器。当优化器检测到您基本上没有引起任何全局副作用时,您可能会陷入微基准测试(例如:计算数据只是为了丢弃它而不打印它或对它做任何事情以在其他地方引起变化)。使用非常复杂的优化器,它们可以检测到何时可以跳过某些计算,因为它们不会产生副作用,而且您有时可能会发现您所做的某些事情使速度 运行 快了 10,000 倍,但这并不是因为实际工作正在完成得更快,但因为优化器认为根本不需要完成并直接跳过它。如果在您的测试中,您设身处地为用户着想,并且可以合理化为什么可以跳过代码的某些部分并仍然为用户提供相同的 output/result 而无需等待那么久,那么优化器也可以并且只需跳过该代码。每当您针对强大的优化器进行微基准测试并发现一个好得令人难以置信的结果时,它可能好得令人难以置信,并且优化器只是直接跳过工作,因为它在您的表面测试中注意到它实际上不需要这样做。
最后但并非最不重要的一点是,关注微观效率通常会让您陷入类似装配的思维。当您只是在紧密循环中重复测试一些指令时,没有回旋余地以更智能的方式进行优化。通常,在关注性能测量时,您需要更多的回旋余地,从粗略的优化(算法、多线程等)到最小的微优化。当你的测试是微观的时候,你最终会立即进入最精细的金属刮擦思维,这可能会导致不健康的痴迷于节省几个时钟周期,而现实世界可能会给你一个节省数十亿的机会与相同的 effort/time 投资。
在研究数组分配性能时,我偶然发现了这个 jsperf,据称它显示了 for
形式循环的非常显着的速度提升:
var i, j = 0;
for (i = 0; i < n; i++) {
myArray[j++] = i;
}
...在当前的浏览器中。这种 "double increment" 形式对我来说完全陌生,看起来很奇怪。我找不到任何关于它的讨论。
Many people 多年来一直警告说,由于编译器优化的越来越聪明,来自微基准测试的误导性数据。这是其中之一吗?为什么或者为什么不?如果 jsperf 写错了,我很乐意看到一个正确的、修改后的 jsperf,它能更准确地反映现实世界的情况。
除非 JS 正在做一些非常 poor/bizarre 的优化,否则这种双计数器循环与单计数器循环相比应该没有更快的方法。从机器级别的角度来看,这样的代码通常会使用两个通用寄存器而不是一个,必须递增两个。差异应该很小,一般来说可以忽略不计,但单个计数器应该有轻微的性能优势。
唯一确定的方法是查看生成的机器 instructions/disassembly。
使用 length
可能不像访问变量那么简单。如果是这样的话,我有点惊讶,但它可能会转化为更多的指令(例如:在最坏的情况下涉及分支)。但是在那种情况下,您仍然应该使用 i
而不是 j
并仍然是一个单计数器循环来获得稍微更快的结果。
值得尝试的一件事是改变测试的顺序。之前与 paging/caching 相关的测试的内存可能发生了一些变化,这使得后面的双计数器测试进行得更快。例如,双计数器测试后立即进行单计数器测试。尝试按执行顺序交换这两个,看看它是否会影响结果。
更新
如评论中 Mark 的测试所示,push-numbers-redux,他在避免 length
时使用单个计数器循环确实获得了更快的结果。显然 length
确实需要比简单变量更多的指令,也许对于优化器无法消除的关联数组情况有一些分支。但是,如果我们正在针对变量测试常规变量,那么单计数器循环仍将击败双计数器循环。
微基准测试
由于还提出了关于为什么微基准测试可能有些糟糕的主题,因此它们并不总是很糟糕,但有一些与之相关的警告。我会说您的测试非常微观,因为从用户的角度来看它没有做任何有意义的事情。它创建了一个数据数组,但什么都不做。
如果您试图从此类测试中概括性能想法,为什么这会很糟糕,首先,硬件是一台动态机器。它试图预测你在做什么,哪些代码分支会更常执行,将 DRAM 传输到缓存等。操作系统也是动态的,动态分页内存。考虑到环境的动态特性,您可能会面临编写微型测试的危险,该测试看起来速度更快,但只是对这些动态因素感到幸运。做更多工作的真实世界测试,也许更重要的是,各种各样的工作,往往会减轻 "dynamic luck" 可能误导你相信某些东西通常更快的因素,而实际上它可能只对你的特定测试更快。您不必编写成熟的大型应用程序来获得类似真实世界的东西。例如,计算 Mandelbrot 集是相当简单的代码(可以放在一页中),但仍然足以避免那些微观层面的危险。
另一个危险是优化编译器。当优化器检测到您基本上没有引起任何全局副作用时,您可能会陷入微基准测试(例如:计算数据只是为了丢弃它而不打印它或对它做任何事情以在其他地方引起变化)。使用非常复杂的优化器,它们可以检测到何时可以跳过某些计算,因为它们不会产生副作用,而且您有时可能会发现您所做的某些事情使速度 运行 快了 10,000 倍,但这并不是因为实际工作正在完成得更快,但因为优化器认为根本不需要完成并直接跳过它。如果在您的测试中,您设身处地为用户着想,并且可以合理化为什么可以跳过代码的某些部分并仍然为用户提供相同的 output/result 而无需等待那么久,那么优化器也可以并且只需跳过该代码。每当您针对强大的优化器进行微基准测试并发现一个好得令人难以置信的结果时,它可能好得令人难以置信,并且优化器只是直接跳过工作,因为它在您的表面测试中注意到它实际上不需要这样做。
最后但并非最不重要的一点是,关注微观效率通常会让您陷入类似装配的思维。当您只是在紧密循环中重复测试一些指令时,没有回旋余地以更智能的方式进行优化。通常,在关注性能测量时,您需要更多的回旋余地,从粗略的优化(算法、多线程等)到最小的微优化。当你的测试是微观的时候,你最终会立即进入最精细的金属刮擦思维,这可能会导致不健康的痴迷于节省几个时钟周期,而现实世界可能会给你一个节省数十亿的机会与相同的 effort/time 投资。