Question for v8 experts.
I noticed, that array literals instantiation seems to be very un-optimized, compared to the class instantiation. This is very weird, since constructing class instance involves executing constructor code - and array literals do not need to execute any code and can be allocated very efficiently. I wonder if I'm missing something obvious.
The results I receive in Node 16:
nickolay@frontier:~/workspace/typescript/monopoly$ nvm exec 16 node -r esm --expose-gc src_js/instantiation.js
Running node v16.4.1 (npm v7.18.1)
Garbage collection available.
Instantiate object: 1.325ms ±0.017 i:6 c:328
Instantiate array: 3.303ms ±0.077 i:6 c:138
Array literal performs 2.5 times worse!!
The 1st benchmark exercises the instantiation of this simple class:
class DataType0 {
constructor () {
this.prop00 = object
this.prop01 = 0
this.prop02 = '0'
this.prop03 = false
this.prop04 = true
this.prop05 = object
this.prop06 = 0
this.prop07 = 'true'
this.prop08 = false
this.prop09 = 0
this.prop10 = 0
this.prop11 = 0
this.prop12 = 0
this.prop13 = 0
this.prop14 = 0
this.prop15 = 0
}
}
The 2nd benchmark exercises the instantiation of the array literal with identical elements:
[
object,
0,
'0',
false,
true,
object,
0,
'true',
false,
0,
0,
0,
0,
0,
0,
0,
]
The allocation function looks like this:
//------------------------------------------------------------------------------
const size = 30000;
const generateObject = () => {
const instances = [];
do {
instances.push(__ALLOCATION_CALL_HERE__);
} while (instances.length < size);
return instances;
};
I'd expect the array literal instantiation to perform much better, since it does not need to execute any code and can be allocated with memory copy + some GC tracking. At the very least, I'd expect the array literal to perform on par with class instantiation.
The reason I'm instantiation the 16 properties, is that I read somewhere that in v8 every newly allocated array receives space for 16 elements.
To reproduce:
git@github.com:canonic-epicure/monopoly.git
> npm i
> npx tsc
> node -r esm --expose-gc src_js/instantiation.js
The benchmark file is: src_js/instantiation.js
Question: Am I missing something obvious (and there's a way to instantiate an array more efficiently), or this is indeed a "slow path" in v8?
(V8 developer here.)
TL;DR: It's a microbenchmarking artifact, not reflective of real-world performance. With a slight tweak, the same microbenchmark produces opposite results.
Long version: at first glance, this is a curious case indeed; I would also expect that object and array allocations have the same performance. Then again it's a microbenchmark (and as artifical as they come), and microbenchmarks are generally misleading, so the observed difference is most likely an artifact of the way this specific benchmark has been written, and hence not relevant for less artificial situations.
Specifically, a quick profiling run reveals that this benchmark doesn't primarily measure allocation -- instead, it mostly measures garbage collection, because it stress-tests two highly efficient methods for creating garbage, which naturally creates a lot of garbage.
Playing around with the constants a bit, it turns out that when you set size = 20000
, then arrays appear to be twice as fast as objects, whereas if you keep it at size = 30000
, then objects appear to be twice as fast as arrays; with size = 33000
, arrays appear faster again; with size = 1000
, they have approximately the same speed. (It makes sense that arrays are a tiny bit more expensive, because they have to allocate two heap objects under the hood, whereas in this particular case the DataType0
class gets away with storing all properties in the object itself.)
Since size
affects them, we can deduce that the differences are due to the instances.push(...)
calls, not the object/array allocations themselves; but the instances list is the same in both cases. I could bore you with speculation (or spend my day with investigation) on what exactly causes these differences (quick guess: different allocation sizes affect timing of new-space GC cycles, which in turn causes differences in subsequent write barrier costliness?), but then again it doesn't really matter -- it's just a misleading microbenchmark, there's nothing actionable to be learned here.
constructing class instance involves executing constructor code - and array literals do not need to execute any code
No, from an engine point of view this does not hold. V8 optimizes simple class constructors to basically the same as literals, so neither of them "executes any code". Then again creating any object instance, no matter the optimization strategy, of course involves making the CPU execute instructions, so as far as the compiler is concerned, they both execute code... at any rate, it's basically the same mechanism, hence ~same performance.
in v8 every newly allocated array receives space for 16 elements.
That's incorrect, but also irrelevant for the case at hand.