For example:
const array = [];
for(let i = 0; i<20000; i++)
array.push({x:1.2, y:3.4, t:123+i});
The hidden class of each element would be: [double, double, small integer].
Are the contents consecutive in RAM? Do they look like this? [double, double, small integer, double, double, small integer, double, double, small integer...]
Either in the array itself, Or if they're referenced (and the array is filled with pointers), are them at least consecutively allocated nearby on the heap? [xxx, double, double, small integer, xxx, double, double, small integer, xxx, double, double, small integer, xxx, ...]
I know they work like this for double precision numbers "PACKED_DOUBLE_ELEMENTS", but want to know the behavior for arrays of objects.
As object hidden classes also have arrays themselves of the object's elements/properties so it gets hard to search for the terms and pinpoint an official resource about this, I've also already seen around 4 hours of video presentations but still couldn't figure it out.
This is related to performance and data locality. I know I could split my array-of-structs into structs-of-arrays and just use smis/doubles given the case but I'm trying to avoid that hassle for the sake of usability.
Thank you.
(V8 developer here.)
In short: no.
Are the contents consecutive in RAM?
Yes, probably, at first, at least some of them.
Do they look like this? [double, double, small integer, double, double, small integer, double, double, small integer...]
No.
Arrays of objects are arrays of pointers. It doesn't matter whether the elements kind is "PACKED_ELEMENTS" or "HOLEY_ELEMENTS"; it also doesn't matter whether all pointed-to objects have the same hidden class or not. (Well, it might matter for other operations that are performed on these objects, but the array doesn't care.)
Since the objects are all allocated chronologically right after each other in your snippet, they will also be allocated right next to each other. But over time, the garbage collector will move them around (possibly even before the loop has finished, if new-space is full), and there is no guarantee that they'll stay near each other. They'll get moved to wherever there's free space on the heap, and that decision will be made for each object individually; so some may stay together in groups by pure chance, others may end up by themselves (surrounded by unrelated objects).
V8 doesn't do unboxing of double fields in objects any more (contrary to array elements), so the x
and y
fields will also be pointers (pointing to HeapNumbers), and just like the previous paragraph described, these HeapNumbers will initially be allocated right next to the objects, but may well be moved elsewhere over time.
Also, the minimum object header before in-object fields is 3 pointers. Putting it all together, you might have a memory layout roughly like:
@100000: pointer to hidden class for "Array with PACKED_ELEMENTS"
@100004: pointer to out-of-object properties (empty array)
@100008: pointer to elements array (@100016)
@100012: 20000 (Smi, Array length)
@100016: pointer to hidden class for "FixedArray"
@100020: 22583 (Smi, FixedArray length, over-allocated for future growth)
@100024: pointer to 1st object (@190356)
@100028: pointer to 2nd object (@190404)
@100032: pointer to 3rd object (@190452)
... <many more here>
@180016: pointer to 19999th object
@180020: pointer to 20000th object
@180024: pointer to <the hole> sentinel
@180028: pointer to <the hole> sentinel
... <many more here>
@190352: pointer to <the hole> sentinel
@190356: pointer to hidden class for "x/y/t"
@190360: pointer to out-of-object properties (empty array)
@190364: pointer to elements array (empty array)
@190368: pointer to @190380 ("x")
@190372: pointer to @190392 ("y")
@190376: 123 (Smi, "t")
@190380: pointer to hidden class for "HeapNumber"
@190384: 1.2 (double, 8 bytes!)
@190392: pointer to hidden class for "HeapNumber"
@190396: 3.4 (double, 8 bytes!)
@190404: pointer to hidden class for "x/y/t"
@190408: pointer to out-of-object properties (empty array)
@190412: pointer to elements array (empty array)
@190416: pointer to @190428 ("x")
@190420: pointer to @190440 ("y")
@190424: 124 (Smi, "t")
@190428: pointer to hidden class for "HeapNumber"
@190432: 1.2 (double, 8 bytes!)
@190440: pointer to hidden class for "HeapNumber"
@190444: 3.4 (double, 8 bytes!)
@190452: hidden class (for "x/y/t")
...
And so on, you get the idea. The details will likely be off (and I wouldn't be surprised if I had a typo somewhere), but that's the general concept.
This is related to performance and data locality. I know I could split my array-of-structs into structs-of-arrays and just use smis/doubles given the case but I'm trying to avoid that hassle for the sake of usability.
Nobody can tell you in the abstract whether that refactoring would be worth doing or a waste of time in the particular case you're looking at. The only way to find out for sure is to try it and measure. It's possible that better data locality improves performance -- but which is better locality? A densely packed array of all the x
values, or having the x
, y
, and t
of one object near each other? Hard to say.