JS 性能:对象字面量函数调用 VS 对象存储在变量中

JS performance: function call with object literal VS object stored in variable

在我的程序中,我通常有很多带有签名 myFunc({ param }) 的函数。我想测试每次使用新对象调用这些函数或使用包含对象的变量调用这些函数之间的区别。即:

myFuncA({ param: 1 });

// vs

const paramObj = { param: 1 };
myFuncB(paramObj);

所以我想出了一个简单的测试:

let a = 0;
let b = 0;

function myFuncA({ param }) {
    a += param;
}

function myFuncB({ param }) {
    b += param;
}

const iterations = 1e9;

console.time('myFuncA');
for(let i = 0; i < iterations; i++) {
    myFuncA({ param: 1 });
}
console.timeEnd('myFuncA')

console.time('myFuncB');
const paramObj = { param: 1 };
for(let i = 0; i < iterations; i++) {
    myFuncB(paramObj);
}
console.timeEnd('myFuncB');

在此测试中,myFuncA 在我的机器上始终更快,这对我来说是违反直觉的。

myFuncA: 2965.320ms
myFuncB: 4271.787ms

myFuncA: 2956.643ms
myFuncB: 4251.753ms

myFuncA: 2958.409ms
myFuncB: 4269.091ms

myFuncA: 2961.827ms
myFuncB: 4270.164ms

myFuncA: 2957.438ms
myFuncB: 4278.623ms

最初我假设在函数调用中创建对象会使迭代变慢,因为您需要创建对象,而不是传递相同的对象每次。不过好像反过来了(?).

我的规格:

为什么会这样?测试有问题吗?

(这里是 V8 开发人员。)TL;DR:微基准测试很少能成功地正确回答您的问题。 (将其中两个放在一起不是这里的问题,尽管它肯定 可以 成为混淆结果的原因。)

这里的一个直接观察是,自 Node 12 以来情况发生了变化。对于当前的 V8 版本(或 Node 14),我发现两种情况下的性能几乎相同,“B”仅慢了约 5%比“A”。当然,你的问题是为什么 B 慢。

Bergi 的猜测是正确的。 V8 可以在执行某些 long-运行 循环时优化函数(这通常是微基准测试模式,很少出现在现实世界的代码中)并执行所谓的“堆栈替换”(OSR)来替换当前正在执行的函数及其优化版本。这显然不能及时返回(重新执行你的代码只想执行一次的事情),所以在循环之前发生的任何事情都是既定的并且不能改变。结合内联和逃逸分析,“A”循环被优化为:

for(let i = 0; i < iterations; i++) {
    a += 1;
}

而“B”循环优化为:

for(let i = 0; i < iterations; i++) {
    b += paramObj.param;
}

所以你真正测量的是具体化常量 1 和从对象加载 属性 之间的区别。

当然,分配一个对象比不分配一个对象要花更多的时间。也就是说,一些对象分配可能会被优化掉。如此简单的微基准测试无法真正告诉您应该如何编写代码以获得最佳性能。

编写高性能代码的有用指南是:

第 1 步:编写对您有意义的代码:易于阅读和理解,便于将来 modify/maintain。在这个阶段记住算法的复杂性是有用的(例如:不要对大于几十个条目的数据集使用二次时间(或更糟的)算法(如冒泡排序)),担心单个机器指令不是。让引擎负责让事情变得更快。 (请注意,这甚至不是特定于 JavaScript;它适用于任何编码。)

第 2 步:如果(且仅当)您察觉到性能问题,分析整个应用程序(在尽可能真实的场景中,特别是为其提供代表性输入数据),以确定大部分时间花在了哪里.

第 3 步:专注于精确优化那些花费最多时间的部分。有时这可能有点间接;例如:如果您注意到很多时间花在了 GC 上,请查看代码中是否有任何地方分配了许多具有中短生命周期的对象,并考虑避免这些分配。现代 JS 引擎具有非常快的分配器和非常快的 GC,因此在大多数情况下,您 不需要 需要特意避免一些对象分配。