gpt4 book ai didi

node.js - 为什么第一个函数调用的执行速度比所有其他顺序调用快两倍?

转载 作者:行者123 更新时间:2023-12-03 12:16:27 26 4
gpt4 key购买 nike

我有一个自定义的 JS 迭代器实现和用于衡量后者实现性能的代码:

const ITERATION_END = Symbol('ITERATION_END');

const arrayIterator = (array) => {
let index = 0;

return {
hasValue: true,
next() {
if (index >= array.length) {
this.hasValue = false;

return ITERATION_END;
}

return array[index++];
},
};
};

const customIterator = (valueGetter) => {
return {
hasValue: true,
next() {
const nextValue = valueGetter();

if (nextValue === ITERATION_END) {
this.hasValue = false;

return ITERATION_END;
}

return nextValue;
},
};
};

const map = (iterator, selector) => customIterator(() => {
const value = iterator.next();

return value === ITERATION_END ? value : selector(value);
});

const filter = (iterator, predicate) => customIterator(() => {
if (!iterator.hasValue) {
return ITERATION_END;
}

let currentValue = iterator.next();

while (iterator.hasValue && currentValue !== ITERATION_END && !predicate(currentValue)) {
currentValue = iterator.next();
}

return currentValue;
});

const toArray = (iterator) => {
const array = [];

while (iterator.hasValue) {
const value = iterator.next();

if (value !== ITERATION_END) {
array.push(value);
}
}

return array;
};

const test = (fn, iterations) => {
const times = [];

for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
times.push(performance.now() - start);
}

console.log(times);
console.log(times.reduce((sum, x) => sum + x, 0) / times.length);
}

const createData = () => Array.from({ length: 9000000 }, (_, i) => i + 1);

const testIterator = (data) => () => toArray(map(filter(arrayIterator(data), x => x % 2 === 0), x => x * 2))

test(testIterator(createData()), 10);

测试函数的输出非常奇怪且出乎意料——第一次测试运行的执行速度总是比其他所有运行快两倍。结果之一,其中数组包含所有执行时间,数字是平均值(我在 Node 上运行):

[
147.9088459983468,
396.3472499996424,
374.82447600364685,
367.74555300176144,
363.6300039961934,
362.44370299577713,
363.8418449983001,
390.86111199855804,
360.23125199973583,
358.4788999930024
]
348.6312940984964

使用 Deno 运行时可以观察到类似的结果,但是我无法在其他 JS 引擎上重现这种行为。 V8 背后的原因是什么?

环境: Node v13.8.0、V8 v7.9.317.25-node.28、Deno v1.3.3、V8 v8.6.334

最佳答案

(此处为 V8 开发人员。)简而言之:内联或缺乏内联,由引擎启发式决定。

对于优化编译器,内联被调用的函数可以带来显着的好处(例如:避免调用开销,有时使常量折叠成为可能,或消除重复计算,有时甚至为额外的内联创造新的机会),但会带来成本:它使编译本身变慢,并且增加了以后由于某些假设不成立而不得不丢弃优化代码(“去优化”)的风险。什么都不内联会浪费性能,内联所有东西都会浪费性能,内联完全正确的功能需要能够预测程序的 future 行为,这显然是不可能的。所以编译器使用启发式方法。

V8 的优化编译器目前具有启发式内联函数,仅当它始终是在特定位置调用的相同函数时。在这种情况下,这是第一次迭代的情况。随后的迭代然后创建新的闭包作为回调,从 V8 的角度来看,它们是新函数,因此它们不会被内联。 (V8 实际上知道一些高级技巧,允许它在某些情况下删除来自同一源的重复函数实例并内联它们;但在这种情况下这些不适用 [我不确定为什么])。

所以在第一次迭代中,所有内容(包括 x => x % 2 === 0x => x * 2)都被内联到 toArray。从第二次迭代开始,情况不再如此,而是生成的代码执行实际的函数调用。

这可能没问题;我猜想在大多数实际应用中,差异几乎无法测量。 (减少测试用例往往会使这种差异更加突出;但是根据对小型测试的观察来更改较大应用程序的设计通常不是花费时间的最有效方式,最坏的情况下会使事情变得更糟。)

此外,为引擎/编译器手动优化代码是一个困难的平衡。我通常会建议不要这样做(因为引擎会随着时间的推移而改进,而让您的代码更快确实是他们的工作);另一方面,显然存在效率更高和效率更低的代码,为了获得最大的整体效率,每个相关人员都需要尽自己的一份力量,也就是说,您最好尽可能简化引擎的工作。

如果您确实想要微调其性能,您可以通过分离代码和数据来实现,从而确保始终调用相同的函数。例如像你的代码的这个修改版本:

const ITERATION_END = Symbol('ITERATION_END');

class ArrayIterator {
constructor(array) {
this.array = array;
this.index = 0;
}
next() {
if (this.index >= this.array.length) return ITERATION_END;
return this.array[this.index++];
}
}
function arrayIterator(array) {
return new ArrayIterator(array);
}

class MapIterator {
constructor(source, modifier) {
this.source = source;
this.modifier = modifier;
}
next() {
const value = this.source.next();
return value === ITERATION_END ? value : this.modifier(value);
}
}
function map(iterator, selector) {
return new MapIterator(iterator, selector);
}

class FilterIterator {
constructor(source, predicate) {
this.source = source;
this.predicate = predicate;
}
next() {
let value = this.source.next();
while (value !== ITERATION_END && !this.predicate(value)) {
value = this.source.next();
}
return value;
}
}
function filter(iterator, predicate) {
return new FilterIterator(iterator, predicate);
}

function toArray(iterator) {
const array = [];
let value;
while ((value = iterator.next()) !== ITERATION_END) {
array.push(value);
}
return array;
}

function test(fn, iterations) {
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
console.log(performance.now() - start);
}
}

function createData() {
return Array.from({ length: 9000000 }, (_, i) => i + 1);
};

function even(x) { return x % 2 === 0; }
function double(x) { return x * 2; }
function testIterator(data) {
return function main() {
return toArray(map(filter(arrayIterator(data), even), double));
};
}

test(testIterator(createData()), 10);

观察热路径和“公共(public)接口(interface)”(即 arrayIteratormapfiltertoArray compose) 与以前完全相同,只是底层细节发生了变化。为所有函数命名的一个好处是您可以获得更有用的分析输出 ;-)

精明的读者会注意到这种修改只会转移问题:如果您的代码中有多个地方使用不同的修饰符/谓词调用 mapfilter,那么inlineability 问题将再次出现。正如我上面所说:微基准测试往往具有误导性,因为真实的应用程序通常具有不同的行为......

(FWIW,这与 Why is the execution time of this function call changing? 的效果几乎相同。)

关于node.js - 为什么第一个函数调用的执行速度比所有其他顺序调用快两倍?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63782775/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com