Translator: Jiang Haitao
In this blog post, we’ll explain how V8 handles JavaScript properties internally. From a JavaScript perspective, there are only a few necessary differences between properties. JavaScript objects are similar to dictionaries in that they use strings as keys and arbitrary objects as values. However, when iterating over an object, the specification iterates differently for properties with integers as indexes than for other properties. Other than this (iterative approach), the different properties behave essentially the same regardless of whether they are integers as indexes.
However, the V8 underlying layer does rely on several different representations of properties for performance and memory reasons. In this blog post, we’ll explain what V8 does for quick access to dynamically added properties. Understanding how properties work is crucial to explaining optimizations like inline caches in V8.
This article explains the difference between V8’s handling of properties with integer as index and named properties. We then showed how V8 maintains HiddenClasses when adding named properties, which allows you to quickly identify the characteristics of an object. Then we’ll go into more detail on how to optimize named properties for quick access or modification based on usage. In the final section, we’ll talk more about how V8 handles properties or array indexes with integers as indexes.
Named properties and Elements
Let’s start by examining a very simple object, such as {a: ‘foo’, b: ‘bar’}. This object has two named properties, ‘a’ and ‘b’. It doesn’t have any array index for the properties name. The array index properties(commonly called elements) are the most prominent feature of arrays. For example, the array [‘foo’, ‘bar’] has two array index properties: 0, whose value is ‘foo’; 1, its corresponding value is ‘bar’. V8 mainly uses this as the first layer of distinction between properties.
The following figure shows what a basic JavaScript object looks like in memory.
Elements and Properties exist in two separate data structures, making it more efficient to add/access properties and Elements in different usage modes.
Elements are used for various array. prototype methods, such as POP or slice. Since these functions access properties on a continuous scale, V8 internally treats them as simple arrays in most cases. Later in this article, we’ll explain how we converted these simple arrays into sparse dictionaries to save memory.
Named properties exist in a separate array in a similar way. But unlike Elements, we can’t simply use keys to infer their position in the Properties array; we need some additional metadata. In V8, each JavaScript object is associated with a hidden class. The hidden class holds information about the characteristics of the object, as well as a map of the properties array index corresponding to the properties name. To complicate things, we sometimes use dictionaries instead of simple arrays to represent properties. We will explain this in detail in the dedicated section.
Content of this section:
- The properties of arrays with integers as indexes exist in a separate Elements store.
- Named properties exist in the Properties Store.
- Elements and properties can be arrays or dictionaries.
- Each JavaScript object has an associated hidden class that holds information about the object’s characteristics.
HiddenClasses and DescriptorArrays
After explaining the general difference between Elements and named properties, we need to take a look at how hidden classes work in V8. This hidden class holds meta-information about an object, including the number of properties on the object and references to the object stereotype. Hidden classes are conceptually similar to classes in typical object-oriented programming languages. But in prototype-based languages such as JavaScript, it’s usually impossible to know this class in advance. So in this case, V8 creates hidden classes on the fly and updates them dynamically as the object changes. Hidden classes are used to identify object characteristics and are also important for V8 compiler optimizations and inline caching. For example, if the optimization compiler can ensure compatible object structures by hiding classes, it can have direct access to the inline properties of the object.
Let’s take a look at the important part of the hidden class.
In V8, the first field of a JavaScript object points to a hidden class. (In fact, this is true of any object that is on the V8 heap and managed by the garbage collector.) In terms of properties, the most important information is the field in the third bit of the hidden class that holds the number of properties and a pointer to the descriptor array. The descriptor array contains information about the named properties, such as the name of the properties and where the value is stored. Note that we are not tracking integer index properties here, so there are no entries in the descriptor array.
The basic setup for hidden classes is that objects have the same structure, such that the same named properties in the same order share the same hidden class. To do this, we use a different hidden class when an attribute is added to the object. In the example below, we create an empty object and add three named properties.
Each time a new property is added, the object’s hidden class changes. V8 creates a transformation tree behind the scenes that links the hidden classes together. When the attribute'a'
When adding to an empty object, V8 knows which hidden class to use. If the same attributes are added in the same order, this transformation tree ensures that the resulting hidden class is the same at the end. The following example shows that the conversion tree remains the same even when simple index properties(that is, integer subscripts) are added between the two.
However, if we create a new object with additional attributes, in this case properties'd'
V8 will create a separate branch for the new hidden class.
Content of this section:
- Objects with the same structure (the same properties in the same order) have the same hidden classes.
- By default, each new named attribute added results in the creation of a new hidden class.
- Adding this property to the array index does not create a new hidden class.
Three different named properties
After an overview of how V8 uses hidden classes to record the characteristics of objects, let’s delve into how these properties are actually stored. As mentioned in the introduction above, there are two basic types of properties: named and Indexed. The following sections describe named properties.
In V8, simple objects like {a: 1, b: 2} can have different internal representations. Although JavaScript objects behave more or less like external simple dictionaries, V8 tries to avoid dictionaries because they hinder certain optimizations, such as inline caching, as we’ll explain in a separate article.
Object propertieswithCommon propertiesV8 supports so-called in-object properties, which are stored directly on objects. These are the fastest attributes in V8 because they are available without any indirect access. The number of properties within an object is determined by the initial size of the object. If they add properties that require more space than the object can store, they store them in the Properties Store. Properties in the Properties Store are indirectly accessible, and the Properties Store can add properties on its own.
Soon the propertieswithSlow the propertiesThe next important distinction is between fast properties and slow properties. Properties stored in a linear Properties store are usually defined as “fast”. Fast Properties is only accessible through index in the Properties Store. To get the actual location of the named properties in the Properties Store, we must refer to the descriptor array on the hidden class, as described earlier.
But if an object adds and removes many attributes, it can take a lot of time and memory overhead to maintain descriptor arrays and hidden classes. As a result, V8 also supports so-called slow properties. An object with slow properties has its Properties Store as a separate dictionary. All properties meta-information is no longer stored in the descriptor array of the hidden class, but directly in the Properties dictionary. So attributes can be added and removed without updating the hidden class. Since inline caching does not apply to dictionary properties, the latter are typically slower than fast properties.
Content of this section:
- There are three different named properties types: in-object properties, fast properties, and slow/dictionary properties.
- In-object properties are stored directly on the object and provide the fastest access.
- Fast Properties is located in the Properties Store, and all meta information is stored in the descriptor array of the hidden class.
- Slow properties resides in a separate Properties dictionary and no longer shares meta-information through hidden classes.
- Slow properties allows efficient deletion and addition of properties, but slower access than the other two types.
Elements or array index properties
So far, we’ve looked at named properties, not the integer index properties commonly used in arrays. The handling of integer index properties is just as complex as that of named properties. Even though all index properties are always stored separately in the Elements Store, there are 20 different Elements types!
Packed or Holey Elements: The first major difference V8 makes is whether the item in the Elements backing store is Packed or holed. If you delete an element that can be accessed by index or an undefined element, a hole will appear in the backing Store. A simple example is [1,,3], where the second term is a hole. The following example illustrates this problem:
const o = ['a'.'b'.'c'];
console.log(o[1]); / / print 'b'
delete o[1]; // Introduce a hole in the Elements Store
console.log(o[1]); / / print 'undefined'; Attribute 1 does not exist
o.__proto__ = {1: 'B'}; // Define attribute 1 on prototype
console.log(o[0]); / / print the 'a'
console.log(o[1]); / / print 'B'
console.log(o[2]); / / print 'c'
console.log(o[3]); / / print undefined
Copy the code
In short, if the array/object being manipulated has no properties on it, then you have to keep looking up the prototype chain. Since elements are independent, we don’t store the index properties of the current array on the hidden class, and we need a special value called the_hole to mark nonexistent attributes. This is critical for the performance of array methods. If we know that there is no hole, that is, the Elements Store is already packed, we can perform local operations without having to do performance-wasting lookups on the prototype chain.
Fast elements or dictionary Elements: The second major difference for Elements is whether they are fast mode or dictionary mode. Fast Elements is a simple internal VM array whose attribute index has a map relationship to index in the Elements Store. But this simple form is very wasteful for very large sparse/Holey arrays, which have very few children. In this case, we use the dictionary-based form to save memory, but at the expense of slightly slower access:
const sparseArray = [];
sparseArray[9999] = 'foo'; // Create an array with dictionary elements
Copy the code
In this example, allocating a full array with 10K children would be wasteful. Instead, V8 creates a dictionary in which to store triples like key-value-descriptor. In this case, the key is ‘9999’ and the value is ‘foo’, and the default descriptor is used. Since there is no way to store descriptor details on hidden classes, V8 uses slow elements whenever a custom descriptor is used to define index properties:
const array = [];
Object.defineProperty(array, 0, {value: 'fixed' configurable: false});
console.log(array[0]); / / print 'fixed'.
array[0] = 'other value'; // Index 0 cannot be overwritten.
console.log(array[0]); // Still print 'fixed'.
Copy the code
In this example, we have added a non-configurable property to the array. This information is stored in the descriptor section of the triplet of the Slow Elements dictionary. It is important to note that array methods are much slower to execute on objects with slow elements.
Smi vs. Double Elements: There is another important distinction between fast Elements in V8. For example, if you store only integers in a commonly used array, GC doesn’t have to look at that array, because integers are encoded directly into what are called Small integers — Smis. Another special case is an array that contains only doubles. Unlike Smis, floating point numbers are typically represented as complete objects that are several words long. But V8 stores raw double values for pure double arrays to avoid memory and performance overhead. The following example lists four examples of Smi and double elements:
const a1 = [1.2.3]; // Smi Packed
const a2 = [1.3]; // Smi Holey, a2[1] on prototype
const b1 = [1.1.2.3]; // Double Packed
const b2 = [1.1.3]; // Double Holey, b2[1] on prototype
Copy the code
Special Elements: The information we have provided so far covers 7 of the 20 different elements types. For simplicity, we removed nine elements from TypedArrays, elements from multiple String Wrappers, and special elements from arguments objects.
ElementsAccessor: as you might think we don’t want to write array methods 20 times in C++, once for each element type. This is where the magic of C++ comes in. Instead of implementing array methods over and over again, we built ElementsAccessor, where we just need to implement simple functions to access elements in the Backing Store. ElementsAccessor requires CRTP to create the specified version for each array method. Therefore, if a method such as slice is called on an array, V8 internally calls a built-in function written in C++ and dispatches it to the specified version of the function via ElementsAccessor:
Content of this section:
- Index properties and Elements for fast mode and dictionary mode.
- Fast properties can be packed, and they can also have holes, which indicate that some index properties have been deleted.
- Elements’ Content is specifically designed to speed up the execution of array methods and reduce GC overhead.
Understanding how Properties works is key to understanding many of the optimizations in V8. There are many internal decisions that are not directly visible to JavaScript developers, but they explain why some code patterns are faster than others. Changing the properties or Elements types often causes V8 to create different hidden classes, which can lead to type contamination that prevents V8 from generating the best code.
Note: This article is translated from the official article of Fast Properties in V8 about object property memory allocation policy