Be aware of Arrays - V8 engine advice
Alireza Ebrahimkhani
Posted on February 13, 2024
In this blog, I decided to talk about arrays and their behavior inside v8. By understanding these you can write efficient code that is good for v8 to optimize.
In V8(the JavaScript engine behind Google Chrome and Node.js), "elements kinds" refer to the internal classifications used to optimize array operations. V8 uses these classifications to make assumptions about the types of elements an array contains, which in turn allows it to optimize access to and manipulation of these arrays. Understanding elements kinds can be crucial for developers looking to write high-performance JavaScript code, as certain operations can cause an array to transition between kinds, potentially impacting performance.
While running JavaScript code, V8 keeps track of what kind of elements each array contains. This information allows V8 to optimize any operations on the array specifically for this type of element. For example, when you call reduce
, map
, or forEach
on an array, V8 can optimize those operations based on what kind of elements the array contains.
V8 categorizes arrays into different "elements kinds" based on the types of values they store and whether they have "holes" (missing elements). This classification allows V8 to use more efficient storage and access methods for arrays, depending on their content. Here's a closer look at each type:
Packed vs. Holey
Packed Elements: These arrays have no missing elements between the first and last defined positions. Access to packed arrays is typically faster because V8 can optimize memory layout and access patterns, assuming a continuous block of elements for example: const array = [1, 2, 3];
Holey Elements: These arrays contain holes, or undefined positions, which can occur if elements are deleted or if an array is declared with a larger initial size than the number of elements it contains. Operations on holey arrays are generally slower because V8 must check for the presence of elements before accessing them for example if you write code like the below sample v8 will treat this array like a Holey element:
Types of Elements
Within the packed and holey classifications, arrays are further categorized by the types of elements they store:
Smi Elements: "Smi" stands for "small integer," referring to a specific optimization for storing 31-bit integers directly within pointers, saving space and access time. Arrays that exclusively contain Smi values are optimized differently from those containing other types of values.
Double Elements: These arrays store floating-point numbers. Because floating-point numbers require a different storage format than integers or other types, V8 optimizes arrays that solely contain doubles differently.
Elements: This category is for arrays that can contain elements of any type, including objects, strings, and symbols, in addition to numbers. These arrays are the most flexible in terms of the types of values they can contain but may not benefit from some of the optimizations that more specialized arrays do.
The elements kinds lattice
V8 implements this tag transitioning system as a lattice. Here’s a simplified visualization of that featuring only the most common elements kinds:
It’s only possible to transition downwards through the lattice. Once a single floating-point number is added to an array of Smis, it is marked as DOUBLE, even if you later overwrite the float with a Smi. Similarly, once a hole is created in an array, it’s marked as holey forever, even when you fill it later.
In general, more specific elements kinds enable more fine-grained optimizations. The further down the elements kind is in the lattice, the slower manipulations of that object might be. For optimal performance, avoid needlessly transitioning to less specific types — stick to the most specific one that’s applicable to your situation.
Performance tips
In most cases, elements kind tracking works invisibly under the hood and you don’t need to worry about it. But here are a few things you can do to get the greatest possible benefit from the system.
- Avoid reading beyond the length of the array
Nowadays, the performance of both for-of
and forEach
is on par with the old-fashioned for loop and when the collection you’re looping over is iterable just use for-of
and for arrays specifically, you could use the forEach
built-in.
- Avoid elements kind transitions
In general, if you need to perform lots of operations on an array, try sticking to an elements kind that’s as specific as possible, so that V8 can optimize those operations as much as possible for example, just adding 1.1 to an array of small integers is enough to transition it to PACKED_DOUBLE_ELEMENTS.
The same thing goes for NaN
and Infinity
. They are represented as doubles, so adding a single NaN
or Infinity
to an array of SMI_ELEMENTS transitions it to DOUBLE_ELEMENTS.
- Avoid polymorphism
If you have code that handles arrays of many different elements kinds, it can lead to polymorphic operations that are slower than a version of the code that only operates on a single elements kind.
Consider the following example, where a library function is called with various elements kinds. (Note that this is not the native Array.prototype.forEach)
Built-in methods (such as Array.prototype.forEach
) can deal with this kind of polymorphism much more efficiently, so consider using them instead of userland library functions in performance-sensitive situations.
- Avoid creating holes
This is one of the most important tips you have to consider because once the array is marked as holey, it remains holey forever — even if all its elements are present later!
Holey arrays occur when there are missing elements in the array, leading to less efficient access and manipulation due to the engine's need to handle potential "holes." Here’s how you can avoid creating holey arrays:
If you know the size of the array in advance but initialize it with empty slots, it becomes a holey array. Instead, pre-initialize it with known values. For integers, you might use 0 or another placeholder value. For a more concise way to initialize arrays without creating holes, use the fill method. This is especially useful when the array size is known, but you want to avoid holes.
Also deleting elements from an array can introduce holes, transforming it into a holey array. If you need to remove an element, consider setting it to undefined
or null
if you cannot use methods like splice
to restructure the array without leaving gaps.
and for the last one when adding new elements to an array, use methods like push
or unshift
instead of directly setting them by index, which could create holes if the index is beyond the current array length.
When writing performance-sensitive JavaScript code, understanding how these array types affect performance can guide how you structure your data. Keeping arrays homogenous (all Smi or all doubles) when possible can leverage V8's optimizations for faster access and manipulation.
Fun part :))
For debugging elements kinds to figure out a given object’s “elements kind”, get a debug build of v8 (either by building from source in debug mode or by grabbing a precompiled binary using jsvu), and run:
out/x64.debug/d8 --allow-natives-syntax
Note that “COW” stands for copy-on-write, which is yet another internal optimization. :))
And, in the end hope you enjoy this blog and learn new things about v8 and its magics behind javascript.
Posted on February 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.