问题描述
背景:我在使用findindex函数处理大小数组时遇到了这个问题。我在下面给出了一个最小的工作示例。我可以避免此问题,但我只是不明白为什么该问题首先存在。
在node.js(v12.16.3)中,为什么在本示例中摆脱围绕find函数的for循环会导致性能急剧提高? (从5600毫秒减少到250毫秒)
如果将第二个数组中的值从1e10更改为1e9或更小,或者将第一个数组中的值从1e10更改为1e10或更多,则不会出现此问题。
const nSims = 1e8
const arrays = [];
arrays[0] = [1];
arrays[1] = [1e10];
console.time('a')
for (var i = 0; i < nSims; i++) {
for (var j = 0; j < 2; j++) {
arrays[j].find((value) => value > 0);
}
}
console.timeEnd('a') // 5600 ms
console.time('b')
for (var i = 0; i < nSims; i++) {
arrays[0].find((value) => value > 0);
arrays[1].find((value) => value > 0);
}
console.timeEnd('b') // 250 ms
解决方法
V8开发人员在这里。
“慢速情况”是通过回调调用Array.find
的真实成本:对于每个元素,Array.find
的内置实现都会执行对提供的回调的调用。除了完成您要求执行的基本工作之外,实际上Array.find
内置的和提供的回调都对实现进行了相当优化。
快速案例受益于V8中的某些其他优化:如果对Array.find
的调用仅见过相同类型的数组(包括内部表示,请参见下文),则类型反馈中会有一些特殊处理收集系统和优化编译器发出它的特殊内联版本,这特别具有以下好处:它还可以内联专门提供给此类数组的回调。正如您在此处看到的那样,该优化在适用时可大大提高速度。
[1e9]
和[1e10]
是不同类型的数组的原因是因为1e9
是一个30位整数,因此V8内部选择了“小整数”(又名“ smi“,该数组元素的31位带符号的int)表示形式。 1e10
不过需要34位,因此V8为数组元素选择了双精度(64位浮点)表示。因此,如果发生Array.find
的相同遇到[1e9]
(或者与此相关的[1]
)和 [1e10]
,它决定“我在这里看到了不止一种类型的数组,内联多个特殊情况可能花费的成本超出其价值,让我们使用通用版本”。您可能会说这种决定在这种情况下有点过于悲观,但这就是试探法的本质:引擎需要规则来决定要做什么,并且由于它们无法预测将来代码将要做什么,因此它们只是必须做出一些猜测-可能是一个很好的猜测,或者不是一个很好的猜测:-)
这与循环本身无关;遍历数组列表只是使同一Array.find
遇到几种数组类型的一种方法。您可以使用通过不同输入调用的函数来触发通用路径的回退,而无需循环。否则您可能会遇到循环(循环遍历其他事物),而仍然停留在快速路径上。
@安东写道:
看来,查找方法有一些问题。
我不会那样说。引擎将Array.find
优化到与手写for循环相同的程度并不容易,例如,因为引擎通常无法将用户提供的回调内联到内置函数中。如上所述,V8知道有足够的技巧可以在某些情况下(但并非总是如此)进行这种内联。
这远不是唯一一种用手写替代内置功能可以实现更快性能的情况。在许多情况下,这是因为内置功能比手写替换更通用(即:支持更多奇怪的角落情况)。还有一种情况是,在有针对性的微基准之外,很难找到一种差异实际上很重要的情况。(虽然当然不是不可能)。
,注意:也许这不是正确的答案,但这只是一个很大的注释(我需要代码片段来说明)。
这是问题的示例(a
花费超过5秒,b
花费不到2秒):
const nSims = 1e8
const arrays = [];
arrays[0] = [1];
arrays[1] = [1e10];
console.time('a')
for (var i = 0; i < nSims; i++) {
for (var j = 0; j < 2; j++) {
arrays[j].find((value) => value > 0);
}
}
console.timeEnd('a') // 5600 ms
console.time('b')
for (var i = 0; i < nSims; i++) {
arrays[0].find((value) => value > 0);
arrays[1].find((value) => value > 0);
}
console.timeEnd('b') // 250 ms
如果我们将1e10
更改为1e9
(在此为“魔术”),则会发生这种情况:
const nSims = 1e8
const arrays = [];
arrays[0] = [1];
arrays[1] = [1e9];
console.time('a')
for (var i = 0; i < nSims; i++) {
for (var j = 0; j < 2; j++) {
arrays[j].find((value) => value > 0);
}
}
console.timeEnd('a') // 5600 ms
console.time('b')
for (var i = 0; i < nSims; i++) {
arrays[0].find((value) => value > 0);
arrays[1].find((value) => value > 0);
}
console.timeEnd('b') // 250 ms
似乎find
方法存在一些问题。在这里,如果我们将其替换为for
迭代(a
和b
接近并且小于1秒),将会发生什么情况:
const nSims = 1e8
const arrays = [];
arrays[0] = [1];
arrays[1] = [1e10];
function find(arr) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] > 0) return arr[i];
}
}
console.time('a')
for (var i = 0; i < nSims; i++) {
for (var j = 0; j < 2; j++) {
find(arrays[j]);
}
}
console.timeEnd('a')
console.time('b')
for (var i = 0; i < nSims; i++) {
find(arrays[0]);
find(arrays[1]);
}
console.timeEnd('b')