(这里是 V8 开发人员。)简而言之:它是内联还是缺乏内联,由引擎启发式决定。
对于优化编译器来说,内联被调用的函数可以带来显着的好处(例如:避免调用开销,有时使常量折叠成为可能,或消除重复计算,有时甚至为额外内联创造新的机会),但需要付出代价:使编译本身变慢,并且由于某些假设不成立而增加了稍后不得不丢弃优化代码(“取消优化”)的风险。内联什么都不会浪费性能,内联所有东西都会浪费性能,内联正确的函数需要能够预测程序的未来行为,这显然是不可能的。因此编译器使用启发式方法。
目前,V8 的优化编译器仅在特定位置调用的函数始终相同时才会对内联函数进行启发式处理。在本例中,这是第一次迭代的情况。随后的迭代会创建新的闭包作为回调,从 V8 的角度来看,它们是新函数,因此它们不会被内联。 (V8 实际上知道一些高级技巧,允许它在某些情况下消除来自同一源的重复函数实例并内联它们;但在这种情况下,这些技巧不适用 [我不确定为什么])。
所以在第一次迭代中,一切(包括x => x % 2 === 0
and x => x * 2
) 被内联到toArray
。从第二次迭代开始,情况不再如此,而是生成的代码执行实际的函数调用。
这可能没问题;我猜想在大多数实际应用中,差异几乎无法衡量。 (减少测试用例往往会使这种差异更加突出;但是根据小测试的观察结果更改较大应用程序的设计通常并不是最有效的消磨时间的方式,最坏的情况下可能会让事情变得更糟。)
此外,手动优化引擎/编译器的代码是一个困难的平衡。我一般会推荐not这样做(因为引擎会随着时间的推移而改进,而让你的代码变得更快确实是他们的工作);另一方面,显然存在效率更高的代码和效率较低的代码,为了最大程度地提高整体效率,每个参与人员都需要尽自己的一份力量,也就是说,您最好尽可能让引擎的工作变得更简单。
如果您确实想微调其性能,可以通过分离代码和数据来实现,从而确保始终调用相同的函数。例如,您的代码的修改版本如下:
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);
观察热路径上如何不再动态创建函数以及“公共接口”(即arrayIterator
, map
, filter
, and toArray
compose)与以前完全相同,只是底层细节发生了变化。给所有函数命名的好处是您可以获得更有用的分析输出;-)
精明的读者会注意到,这种修改只会转移问题:如果代码中有多个地方调用map
and filter
使用不同的修饰符/谓词,那么内联性问题将再次出现。正如我上面所说:微基准往往会产生误导,因为真正的应用程序通常有不同的行为......
(FWIW,这与为什么这个函数调用的执行时间会改变? https://stackoverflow.com/questions/62704854/why-is-the-execution-time-of-this-function-call-changing .)