This blog explores the nature of objects in JavaScript from a V8 engine perspective, And how the V8 engine leverages some features of compiled languages (such as structs, address offsets, pre-parsing, inline caching, etc.) to optimize access to object properties, which is another example of why using typescript is necessary for projects. In javascript language, any object is composed of two parts: the attribute name and the attribute value. For the attribute name, there is a string type and a number type. For the attribute value, it can be any type. Or you may have heard that objects in JavaScript, also known as dictionaries, are stored and retrieved as key-value pairs. These are the first things we learn about JavaScript, which raises the first question: Dictionary lookup properties are non-linear and very slow compared to compiled languages, which replace variables with offsets or directly address values at compile time. So how can we optimize the lookup properties of dynamic languages to approximate the performance of compiled languages? To answer this question, we’ll start with V8’s internal representation of objects, and then introduce named attributes, element attributes, hidden classes, and in-objects respectively. Finally, it introduces how to use the object’s hidden class to realize inline caching to optimize function performance.
Structure and characteristics
V8 parsing
Install jsvu
- Install node and NPM locally.
- NPM install jsvu -g
- Jsvu :{HOME}/.jsvu: {HOME}/.jsvu: {PATH}”
- Install v8-DEBUG: jsvu — OS = MAC64 –engines= V8-DEBUG
- .jsvu/v8-debug –allow-natives-syntax [js file absolute address]
compiling
To define the index.js file:
const obj = {
1: 'frist'.2:'second'.first: 1.second: 2
};
%DebugPrint(obj);
Copy the code
Jsvu/V8-debug –allow-natives-syntax… /index.js
The following structure information is obtained:
DebugPrint: 0x5e308148d61: [JS_OBJECT_TYPE]
- map: 0x05e30830736d <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x05e3082c3be9 <Object map = 0x5e3083021b5>
- elements: 0x05e308148d75 <FixedArray[19]> [HOLEY_ELEMENTS]
- properties: 0x05e308042229 <FixedArray[0]> {
0x5e3080436cd: [String] in ReadOnlySpace: #first: 1 (const data field 0)
0x5e308043b4d: [String] in ReadOnlySpace: #second: 2 (const data field 1)
}
- elements: 0x05e308148d75 <FixedArray[19]> {
0: 0x05e308042429 <the_hole>
1: 0x05e3082d24f9 <String[5]: #frist>
2: 0x05e308043b4d <String[6]: #second>
3-18: 0x05e308042429 <the_hole>
}
0x5e30830736d: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x05e308307345 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x05e308242445 <Cell value= 1>
- instance descriptors (own) #2: 0x05e308148de5 <DescriptorArray[2]>
- prototype: 0x05e3082c3be9 <Object map = 0x5e3083021b5>
- constructor: 0x05e3082c3821 <JSFunction Object (sfi = 0x5e308248cd1)>
- dependent code: 0x05e3080421b5 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
Copy the code
As you can see, for obj objects, v8 compilation produces a structure with the following attributes:
- map: Hidden classes or what we call hidden classes. Properties used to describe some structures of the object, similar to structures in compiled languages. But hidden classes are only related to named attributes, not element attributes. There are several properties in a hidden class:
- Instance size: The size of an instance of the object, which is similar to the size of a structure in C. This property is declared to be created when the object is initialized to determine whether the named property storage structure is linear and fast or slow access.
- Inobject properties: Number of in-object properties. In-object is a means of optimizing quick access to properties.
- Back Pointer: A hidden class linked list is generated when an attribute is added or deleted. It is used to maintain the link relation of the linked list.
- Instance descriptors: Pointer to the array of descriptors, which holds the named attribute information, such as the name itself and the location of the stored value, when accessing quickly so that the named attribute can be quickly located.
- Prototype: Prototype property of the object.
- Prototype: Object prototype
- Elements: An element attribute of the object. An element attribute is defined as an attribute whose attribute name is a number, which may also be a number string. Elements are typically structured as an array, but degenerate into a dictionary when attributes are added manually. For example, the 1 and 2 properties in obj.
- Properties: The named properties of this object, the named properties are defined as properties whose names are strings. The general properties properties are structured as arrays and dictionaries. If it’s arrays, it’s probably in the hidden class instance Descriptors that the corresponding property values are stored. It can be retrieved directly from the offset based on the storage location. For example, the frist second property in the obj property.
For obj, elements, properties are FixedArray (array type), storing the address map of the frist Second named property in the instance descriptors of the map hidden class. The frist second and frist second named attributes are also in-object. Unused property fields are 20. If an attribute larger than 20 is added, the properties degenerate to NameDictionary, or dictionary. You can modify the code to:
const obj = {
'1': 'frist'.'2':'second'.first: 1.second: 2
};
%DebugPrint(obj);
obj[3] = 'three';
%DebugPrint(obj);
for (let index = 0; index < 25; index++) {
obj['s'+index] = index;
}
%DebugPrint(obj);
Copy the code
So if you run it again, if you add a 3 property to the structure, then the named property and the element property are still of FixedArray array type and it doesn’t degrade the object, but because there’s no space left for the named property, So increasing the s-beginning attributes (which are larger than 20) degrades the named attributes to NameDictionary (dictionary) and instance descriptors to 0, i.e. there is no quick attribute to find by hiding the class.
Browser view
properties
This section introduces an important field in the object’s internal identity, properties. This field describes the named attributes in the object, which are treated differently because of the operational difference between the named attributes and the Elements attributes. For example, elements are more likely to call the array. prototype method on each element, and as long as the length is set artificially, almost all Array methods will be executed. At the same time, Elements is handled and used differently from the Properties property because of its nearly uniform and continuous storage of data types. You may be familiar with C, C++, and other languages that have Pointers to structures. Because these languages determine the properties of objects or the location of elements in an array at compile time, they can execute much faster than parsing languages. For v8’s optimization of javascript objects, elements uses array pointer addressing to quickly determine the location of attributes, and properties uses hidden classes, inline caching, and in-object features to optimize access to named attributes. Makes performance closer to the compiled language. Next, we will mainly introduce the properties attribute. After the object is created by default, V8 will set it as fast attribute by default, and create hidden classes. In addition, elements that are frequently accessed will be treated as in-object, and degenerate into slow access mode when adding and deleting attributes.
Fast attributes with hidden classes
We need to assume a convention that creates objects that don’t add new attributes and delete attributes arbitrarily. Again, v8 uses hidden classes to optimize the speed of access to named attributes. How is that optimized? Let’s start with an example: Suppose we have the following code:
const obj = {
first: 1.second: 2.three: 3
};
%DebugPrint(obj);
Copy the code
The compiled result is:
DebugPrint: 0x139008148789: [JS_OBJECT_TYPE]
– map: 0x139008307345 <Map(HOLEY_ELEMENTS)> [FastProperties]
– prototype: 0x1390082c3bb9
– elements: 0x139008042229 <FixedArray[0]> [HOLEY_ELEMENTS]
– properties: 0x139008042229 <FixedArray[0]> {
0x1390080436cd: [String] in ReadOnlySpace: #first: 1 (const data field 0)
0x139008043b4d: [String] in ReadOnlySpace: #second: 2 (const data field 1)
0x1390082d2459: [String] in OldSpace: #three: 3 (const data field 2)
}
0x139008307345: [Map]
– type: JS_OBJECT_TYPE
– instance size: 24
– inobject properties: 3
– elements kind: HOLEY_ELEMENTS
– unused property fields: 0
– enum length: invalid
– stable_map
– back pointer: 0x13900830731d <Map(HOLEY_ELEMENTS)>
– prototype_validity cell: 0x139008242445
– instance descriptors (own) #3: 0x1390081487e5 <DescriptorArray[3]>
– prototype: 0x1390082c3bb9
– constructor: 0x1390082c37f1 <JSFunction Object (sfi = 0x139008248cfd)>
– dependent code: 0x1390080421b5 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
– construction counter: 0
As you can see, the properties attribute is a linear structure of an array. The map hidden class is used to describe the structure of the named attributes. The instance Descriptors attribute records the structure of the three named attributes: the named field name, the offset, and so on. If you are looking for an attribute, you can locate the attribute directly by hiding the class and quickly find it based on the value offset.
Specifically, we can think of the object obj as an instance of the following type:
Properties is an array containing the values of the first second three properties, 1, 2, and 3. When I get the second property, LOOK at the instance descriptors hash in the hidden class to get the value at second. The process from getting second to getting the value, in which the address is read from the hash table, is linear, whereas using a normal dictionary would be non-linear. So what happens to the hidden class if you add attributes?
As the picture shows,
- When an object is first created, a hidden class is created for the structure of the object. If an attribute is added to the object, that is, the shape of the object changes, a new hidden class is created. The new hidden class points to the previous hidden class through the back pointer and manages the relationship between the hidden classes.
- Objects with the same structure will reuse the same hidden class. By “same”, I mean the same attribute name, the same number of attributes, and the same order in which attributes are added.
- When an attribute is deleted, a new hidden class is created. The hidden class created has nothing to do with the previous hidden class, so frequent deletion will affect the performance of the operation. If you change the value of a property using null fields, no new hidden classes are created, which improves the performance of accessing the property.
in-object
Of course, the above access although using the hidden class to replace the ordinary dictionary nonlinear query to the hidden class hash table location address linear query. However, we still need to go through the map hidden class to query one step, so can we directly locate the element value in one step? V8 uses in-object mode to optimize the attributes of high-frequency operations. Generally, the initial attributes will default to in-object when the object is created and declared. In-object means that these attributes are directly used as attributes of v8 processing objects. The properties of the object are directly used as properties of the compiled V8 internal representation object. The number of in-objects is set when the hidden class is first created. The number of inline properties is specified in the inObject Properties field. Newly added properties may not be inlined. Put it directly in the Properties property.
Inline cache (IC)
Both the hidden class and in-object above are some preconditions generated for optimization in the stage of code precompilation, which is fundamentally to prepare for the performance of execution. Therefore, inline caching is a means to efficiently optimize code execution by using hidden classes when processing code execution. How does V8 handle the following function?
function foo(obj) {
obj.x += 1;
return obj.y;
}
Copy the code
For now, every time we execute foo, we get the object obj to get the hidden class of obj, and then when we execute code obj.x, we get the location of the x property from the hidden class, and we get the value from the offset of the properties property. The boiler process is faster than a nonlinear dictionary, but for frequently executed functions the type of obj is fixed. It would be wasteful to have to do this every time, so V8 maintains some slots for internal objects with corresponding feedback vectors for each function. The point at which an object property is called inside a function is called the call point. So for foo, the call point is obj.x, obj.y. Some information about slot maintainer X and Y is easy to get quickly.
Both attributes x and y maintain the address and offset of the hidden class to which the attribute corresponds. During the internal operation, the map address is quickly found based on the slot number. If the map address is consistent, the value is directly obtained based on the offset. However, for polymorphic functions, the object attributes passed in May be the same but the content, corresponding to the hidden class may be different, so you can cache the polymorphic form for example:
This a slot corresponding to 2 ~ 4 hidden class called polymorphic slot, one slot called singleton, more than four is the super state, for polymorphic super state, if a slot corresponding to hide a lot in comparison to find the hidden class process can cause performance problems, so as far as possible don’t let the unified function more than 2 morphism. It’s better to keep it singly.
The slow property of degradation
Frequent additions and deletions to properties can break the support for hidden classes in properties, and the properties property can be relegated to dictionary mode. It also affects inline caching.
const obj = {
first: 1.second: 2
};
%DebugPrint(obj);
for (let index = 0; index < 25; index++) {
obj['s'+index] = index;
}
%DebugPrint(obj);
Copy the code
The results obtained are:
DebugPrint: 0x1706081487d9: [JS_OBJECT_TYPE]
- map: 0x1706083072f5 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1706082c3bb9 <Object map = 0x1706083021b5>
- elements: 0x170608042229 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x170608042229 <FixedArray[0] > {0x1706080436cd: [String] in ReadOnlySpace: #first: 1 (const data field 0)
0x170608043b4d: [String] in ReadOnlySpace: #second: 2 (const data field 1)}0x1706083072f5: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1706083072cd <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x170608242445 <Cell value= 1>
- instance descriptors (own) #2: 0x170608148809 <DescriptorArray[2]>
- prototype: 0x1706082c3bb9 <Object map = 0x1706083021b5>
- constructor: 0x1706082c37f1 <JSFunction Object (sfi = 0x170608248cfd) > -dependent code: 0x1706080421b5 <Other heap object (WEAK_FIXED_ARRAY_TYPE) > -construction counter: 0
DebugPrint: 0x1706081487d9: [JS_OBJECT_TYPE]
- map: 0x17060830533d <Map(HOLEY_ELEMENTS) > [DictionaryProperties]
- prototype: 0x1706082c3bb9 <Object map = 0x1706083021b5>
- elements: 0x170608042229 <FixedArray[0] > [HOLEY_ELEMENTS]
- properties: 0x1706081490a9 <NameDictionary[197] > {s19: 19 (data, dict_index: 22.attrs: [WEC])
s8: 8 (data, dict_index: 11.attrs: [WEC])
s2: 2 (data, dict_index: 5.attrs: [WEC])
...
}
0x17060830533d: [Map]
- type: JS_OBJECT_TYPE
- instance size: 12
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- dictionary_map
- may_have_interesting_symbols
- back pointer: 0x1706080423b1 <undefined>
- prototype_validity cell: 0x170608242445 <Cell value= 1>
- instance descriptors (own) #0: 0x1706080421bd <DescriptorArray[0]>
- prototype: 0x1706082c3bb9 <Object map = 0x1706083021b5>
- constructor: 0x1706082c37f1 <JSFunction Object (sfi = 0x170608248cfd) > -dependent code: 0x1706080421b5 <Other heap object (WEAK_FIXED_ARRAY_TYPE) > -construction counter: 0
Copy the code
You’ll see that when you add a lot of properties, the properties degenerate to NameDictionary’s literal mode, and the instance descriptors in the hidden class don’t empty to 0. Similarly, a large number of deleete deletions can affect the performance of an object, because each deletion creates a corresponding hidden class.
elements
After introducing properties, it is worth noting that Element is not managed by a hidden class, so the usual storage type for element attributes is array mode, which is used to locate specific attributes by address offset. At the same time, element attributes have natural subscripts, which can be constructed to maintain the mapping of subscript pre-addresses. V8 has also made some improvements to the element attributes:
Holey and declaration elements
For initializing an element attribute, an array of preaddresses is created for the element, but for sparse element attributes, V8 adds a hole flag to indicate that the element is empty, avoiding wasteful resource lookup in the prototype:
const obj = {
1: 'a'.40:'b'
};
%DebugPrint(obj);
Copy the code
DebugPrint: 0x49708148779: [JS_OBJECT_TYPE]
- map: 0x0497083072f5 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0497082c3bb9 <Object map = 0x497083021b5>
- elements: 0x0497081487ed <FixedArray[77]> [HOLEY_ELEMENTS]
- properties: 0x049708042229 <FixedArray[0]> {}
- elements: 0x0497081487ed <FixedArray[77] > {0: 0x049708042429 <the_hole>
1: 0x0497080caa31 <String[1]: #a>
2-39: 0x049708042429 <the_hole>
40: 0x0497080cad0d <String[1]: #b>
41-76: 0x049708042429 <the_hole>
}
0x497083072f5: [Map]
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- elements kind: HOLEY_ELEMENTS
- unused property fields: 4
- enum length: invalid
- stable_map
- back pointer: 0x0497080423b1 <undefined>
- prototype_validity cell: 0x049708242445 <Cell value= 1>
- instance descriptors (own) #0: 0x0497080421bd <DescriptorArray[0]>
- prototype: 0x0497082c3bb9 <Object map = 0x497083021b5>
- constructor: 0x0497082c37f1 <JSFunction Object (sfi = 0x49708248cfd) > -dependent code: 0x0497080421b5 <Other heap object (WEAK_FIXED_ARRAY_TYPE) > -construction counter: 0
Copy the code
As can be seen, except for 1 and 40, all the other parts are marked as holes indicating vacant locations. However, if created declaratively, elements are degraded to a dictionary. For example:
const obj = {
1:'a'.40:'b'
};
Object.defineProperty(obj, '60', {
value: 'c'.enumerable:false.configurable: false.writable: false
});
%DebugPrint(obj);
Copy the code
Enumerable configurable/writable one of them is false, could lead to elements degenerate into a dictionary:
- elements: 0x143008148a71 <NumberDictionary[16]> {
- requires_slow_elements
1: 0x1430080caa31 <String[1]: #a> (data, dict_index: 0.attrs: [WEC])
40: 0x1430080cad0d <String[1]: #b> (data, dict_index: 0.attrs: [WEC])
60: 0x1430082d2479 <String[1]: #c> (data, dict_index: 0.attrs: [_____])}Copy the code
Code practice
- Do not add or delete a lot of objects to avoid the degradation of named attributes to slow attributes.
- The attributes, sequence, and number of objects of the same type must be the same to avoid creating too many hidden classes.
- Keep the function singlet as much as possible.
- Avoid creating element attributes declaratively.
- Use typescript.