Reputation: 35
In my programs I usually have a lot of functions with the signature myFunc({ param })
. I wanted to test what was the difference between calling these functions with a new object every time or with a variable that contains an object. i.e:
myFuncA({ param: 1 });
// vs
const paramObj = { param: 1 };
myFuncB(paramObj);
So I came up with a simple test:
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');
In this test, myFuncA is consistently faster on my machine, which is counter intuitive to me.
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
Initially I assumed creating an object in the function call would make the iterations slower because you need to create the object, rather than pass the same object every time. But it seems to be the other way round (?).
My specs:
Why does this happen? Is there something wrong with the test?
Upvotes: 1
Views: 823
Reputation: 40501
(V8 developer here.) TL;DR: microbenchmarks rarely succeed in answering your question(s) correctly. (Putting two of them together is not the problem here, though it certainly can be a reason for confusing results.)
One immediate observation here is that things have changed since Node 12. With current V8 versions (or Node 14), I'm seeing nearly the same performance in both cases, with "B" only being ~5% slower than "A". Of course, your question is why B is slower at all.
Bergi's guess is spot on. V8 can optimize functions while some long-running loop is executing (which is typically a microbenchmark pattern, rarely showing up in real-world code) and perform so-called "on-stack replacement" (OSR) to replace the currently-executing function with its optimized version. This obviously can't go back in time (to re-execute things your code only wants to execute once), so anything that happened before the loop is a done deal and can't be changed. Combined with inlining and escape analysis, the "A" loop is optimized to:
for(let i = 0; i < iterations; i++) {
a += 1;
}
whereas the "B" loop is optimized to:
for(let i = 0; i < iterations; i++) {
b += paramObj.param;
}
so what you're really measuring is the difference between materializing a constant 1
and loading a property from an object.
Of course, allocating an object takes more time than not allocating an object. That said, some object allocations might get optimized away. So simple microbenchmarks like this one can't really tell you how you should be writing your code for best performance.
Useful guidelines for writing high-performance code are:
Step 1: Write code that makes sense to you: is easy to read and understand, easy to modify/maintain in the future. Keeping algorithmic complexity in mind at this stage is useful (e.g.: don't use quadratic-time (or worse) algorithms (like bubble-sort) for data sets larger than a few dozen entries), worrying about individual machine instructions is not. Let the engine take care of making things fast. (Note that this isn't even specific to JavaScript; it applies to any coding.)
Step 2: If (and only if) you are perceiving performance issues, profile the whole application (in as realistic a scenario as possible, in particular feeding it representative input data), to determine where most time is being spent.
Step 3: Focus on optimizing precisely those parts where most time is being spent. Sometimes this can be a bit indirect; for example: if you notice that a lot of time is spent doing GC, see if there are any places in the code that allocate many objects with short-to-medium lifetime, and think about avoiding those allocations. Modern JS engines have very fast allocators and very fast GCs, so in most cases, you don't need to go out of your way to avoid a few object allocations.
Upvotes: 5