From the perspective of the JavaScript language, a JavaScript object is like a dictionary, with a string as a key name, any object can be used as a key value, and the key value can be read and written by the key name.
However, when V8 implements object storage, the storage method of dictionary is not completely adopted, which is mainly due to performance considerations. Because the dictionary is a non-linear data structure, the query efficiency will be lower than the linear data structure. In order to improve the storage and search efficiency, V8 adopts a set of complex storage strategy .
Today, let’s take a look at those strategies adopted by v8 in order to improve the access performance of objects.
First, let’s analyze the code below
function Foo() {
this[100] = 'test-100'
this[1] = 'test-1'
this["B"] = 'bar-B'
this[50] = 'test-50'
this[9] = 'test-9'
this[8] = 'test-8'
this[3] = 'test-3'
this[5] = 'test-5'
this["A"] = 'bar-A'
this["C"] = 'bar-C'
}
var bar = new Foo()
for(key in bar){
console.log(`index:${key} value:${bar[key]}`)
}
In the above code, we use the constructor Foo to create a bar object. In the constructor, we set many properties to the bar object, including numeric properties and string properties, and then we enumerate the bar object All the attributes, and print them out one by one, the following is the result printed by executing this code:
index:1 value:test-1
index:3 value:test-3
index:5 value:test-5
index:8 value:test-8
index:9 value:test-9
index:50 value:test-50
index:100 value:test-100
index:B value:bar-B
index:A value:bar-A
index:C value:bar-C
Observing this piece of printed data, we find that the order of the printed attributes is not the order we set. When we set the attributes, we set them out of order. For example, we set 100 first, and then set 1, but the output content is not. It is very regular, and in general it is reflected in the following two points:
The set number attributes are printed out first, and are printed in the order of number size;The set string attributes are still printed according to the previous setting order. For example, we set them in the order of B, A, C, and the printing is still in this order.
The reason for this result is that it is defined in the ECMAScript specification
The numeric attributes should be sorted in ascending order of index valueString attributes are sorted in ascending order according to the order in which they were created
Sorting attributes & general attributes & internal attributes
Here we call the numeric attributes in the object sort attributes , which are called elements in V8Paragraph quoted string properties are called regular properties , which are called properties in V8.
In V8, in order to effectively improve the performance of storing and accessing these two attributes, two linear data structures are used to save separately
The numeric attributes are stored in the sort attribute (elements)String properties are stored in the general properties (properties)
We can look at the storage status of the previous case through the Memory of the chrome browser
We can see that in the memory snapshot we only see the sorting attributes (elements)
But no regular properties (properties)
This is because saving different properties into elements and properties, undoubtedly simplifies the complexity of the program.But when looking for elements, there is an extra step
For example, to execute the statement bar.B to find the property value of B, you need to find the object properties pointed to by the properties property, and then find the B property in the properties object. This method adds a step in the search process, so it will Affect the search efficiency of elements.
So V8 has adopted a trade-off strategy to speed up the efficiency of finding attributes.
Some general properties are directly stored in the object itself, we call this in-object properties (in-object properties)
Next, we will learn more about the distribution of objects in memory through Chrome’s memory snapshot
We enter the code below in the console
function Foo(property_num,element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`
this[ppt] = ppt
}
}
var bar = new Foo(10,10)
Switch Chrome Developer Tools to the Memory tab, and click the small circle on the left to capture the current memory snapshot
Enter the constructor Foo in the search box, Chrome will list all objects created by the constructor Foo
Let's observe the layout at this time in the memory snapshot
10 general attributes are stored in the bar function as the internal attributes of the object;The 10 sorting attributes are stored in elements.
Next we can adjust the number of object attributes created to 20
var bar2 = new Foo(20,10)
At this time, the memory layout of the attribute is like this:
10 The attributes are stored directly in the object of bar2;10 general properties are stored in the properties property in a linear data structure;
The 10 numeric attributes are stored in the elements attribute.
Since there are more than 10 common properties created, the other 10 common properties are saved in properties
Note that because there are only 10 properties in properties, it is still a linear data structure
So if there are too many common attributes, such as 100 created, let’s take a look at its memory distribution
var bar3 = new Foo(100,10)
this time, the memory layout of the attribute is like this:
10 The attributes are stored directly in the object of bar3;
90 regular properties are stored in the properties property in this data structure of a non-linear dictionary;
The 10 numeric attributes are stored in the elements attribute.
At this time, the data in the properties property is not stored linearly, but stored in the form of a non-linear dictionary
Next look at the layout after deleting an attribute
var bar4 = new Foo(5,5);
delete bar4.property0
We will find that although only 5 regular properties are set at this time, because we have performed the delete operation, the storage structure in the properties property will also become a non-linear structure
So we can summarize if there are too many attributes in the object
Or there is an operation of repeatedly adding or deleting attributes, V8 will downgrade the linear storage mode to the non-linear dictionary storage mode, which reduces the search speed, but improves the speed of modifying the attributes of the object
Hidden class
Just now we mentioned that V8 has made those improvements to the storage method of objects.
Next, let's talk about what strategy does v8 adopt to improve query efficiency when looking up object attributes
We know that JavaScript is a dynamic language, and its execution efficiency is lower than that of a static language.
In order to improve the execution speed of JavaScript, V8 draws on the features of many static languages, such as the implementation of the JIT mechanism, the introduction of hidden classes in order to increase the speed of object attribute access, and the introduction of inline caching in order to speed up operations.
Let's focus on analyzing the hidden classes in V8 and see how it improves the speed of accessing object attribute values.
Hidden class-static language features
Before we start to study hidden classes, let's analyze why static languages are more efficient than dynamic languages.
A static language needs to define the structure of an object before declaring it, which is also called a shape. The shape of each object is fixed at compile time and cannot be changed. Then when you access the attribute of an object, you will naturally know the offset value of the attribute relative to the address of the object. For example, when using start.x, the compiler will directly write the address of x relative to start into the assembly instruction. Then when the x attribute in the object start is used, the CPU can go directly to the memory address to fetch the content, without any intermediate search links.
When JavaScript is running, the properties of an object can be modified, so when V8 uses an object, such as start.x, it does not know whether there is x in the object, or whether x is relative to the object. What is the offset of, it can also be said that V8 does not know the specific shape of the object.
Then, when you want to query the x property in the object start in JavaScript, V8 will first look up the properties, and then look up the x property in the properties. This process is very slow and time-consuming.
What is Hidden Class?
According to the characteristics of static languages, one of the ideas adopted by v8 is to make the objects in JavaScript static, that is, when V8 runs JavaScript, it will assume that the objects in JavaScript are static. Specifically, V8 does something for each object. The following two assumptions
After the object is created, no new attributes will be added;After the object is created, the attributes will not be deleted.
V8 will create a hidden class for each object. The hidden class of the object records some basic layout information of the object, including the following two points
All the attributes contained in the object;The offset of each attribute relative to the object.
In this way, when V8 accesses an attribute in an object, it will first find the offset of the attribute relative to its object in the hidden class, and then directly go to the memory to retrieve the attribute value without going through a series of The search process of V8 greatly improves the efficiency of V8 searching for objects.
Combine a piece of code to analyze how the hidden class works:
let point = {x:100,y:200}
When V8 executes this code, it will first create a hidden class (also called map) for the point object. Each object has a map attribute whose value points to the hidden class in memory.
The hidden class describes the attribute layout of the object, which mainly includes the attribute name and the offset corresponding to each attribute;
For example, the hidden class of the point object includes the x and y properties, the offset of x is 4, and the offset of y is 8.
The left side of the above figure is the layout of the point object in memory, and the first attribute of the point object points to its map;
After you have the map, when you use point.x again to access the x property,
V8 will query the offset of the x attribute relative to the point object in the map of point
Then add the offset to the starting position of the point object to get the position of the value of the x attribute in the memory. With this position, the value of x is also obtained, so that we can save a more complicated search process.
Multiple objects share a hidden class
We enter the following code in the console, and then view the memory snapshot
function Foo1 () {}
var a = new Foo1()
var b = new Foo1()
a.name = 'aaa'
a.text = 'aaa'
b.name = 'bbb'
b.text = 'bbb'
a[1] = 'aaa'
a[2] = 'aaa'
Both a and b have named attributes name and text, and a has two additional indexable attributes. It is obvious from the snapshot that the indexable attributes are stored in elements. In addition, a and b have the same structure (I marked the position in red after the map)Every object has a map attribute, and the attribute value points to the hidden class of the object. However, if the shape of two objects is the same , V8 will reuse the , which has two advantages:
Reduce the number of creation of hidden classes, and indirectly speed up the execution speed of the code;
Reduce the storage space of hidden classes.
Under what circumstances are the shapes of two objects the same, to meet the following two points:
The same attribute name;The number of equal attributes.
So you may be a little curious about the previous case. The two objects in the front have different attributes (b has two more numeric attributes than a), how can they have the same structure? To understand this problem, you can first think about the following three questions.
Why save the object? Of course it is for later use.What do you need to do when you want to use it? Find this attribute.
What is the purpose of describing the structure? Search by picture, easy to find
So, for indexable attributes, it is already arranged in an orderly manner. Why do we need to search through its structure multiple times in one fell swoop? Since there is no need to search through its structure, we don't need to describe its structure anymore. In this way, it should not be difficult to understand why a and b have the same structure, because their structure only describes the situation where they both have name and text.
Rebuild the hidden class
We mentioned at the beginning that in order to implement hidden classes in V8, two assumptions are required:
After the object is created, no new attributes will be added;After the object is created, the attributes will not be deleted.
However, JavaScript is still a dynamic language. During the execution, the shape of the object can be changed. If the shape of an object changes, the hidden class will also change. This means that V8 must rewrite the newly changed object. Constructing a new hidden class is a big overhead for the execution efficiency of V8.
In layman's terms, adding new properties to an object, deleting properties, or changing the type will change the shape of the object, and it will inevitably trigger V8 to rebuild a new hidden class for the changed shape of the object.
For example, in the previous case, we can try execution (delete a.name)
In this way, we will find that the maps of a and b are different, and the string attributes will be stored in the properties in a non-linear dictionary structure, that is, the internal attributes have become slow attributes.
Best Practices
Combined with the above, if we want to find more efficient, we hope that the hidden class in the object will not be changed casually, because this will trigger V8 to reconstruct the hidden class of the object, which directly affects the execution performance of the program.
Then in actual work, we should try our best to pay attention to the following points:
One, when initializing an object, ensure that the order of attributes is consistent.
For example, don’t create a point object in the order of literals x and y first, and then create an object point2 in the order of literals y and xSecond, try to initialize the complete object properties at once.
Because every time you add a property to an object, V8 will reset the hidden class for the object.Third, try to avoid using the delete method.
The delete method will destroy the shape of the object, and will also cause V8 to regenerate a new hidden class for the object.
Inline caching
Let's analyze the code below
function loadX(o) {
return o.x
}
var o = { x: 1,y:3}
var o1 = { x: 3 ,y:6}
for (var i = 0; i < 90000; i++) {
loadX(o)
loadX(o1)
}
We define a loadX function, it has a parameter o, the function just returns ox.Generally, the process of obtaining ox in V8 is as follows: find the hidden class of object o, find the offset of the x attribute through the hidden class, and then obtain the attribute value according to the offset.
In this code, the loadX function will be executed repeatedly through the for loop, so the process of obtaining ox also needs to be executed repeatedly.
Is there a way to simplify this search process again, it is best to find the attribute value of x in one step?
The answer is: has
V8 will do everything possible to compress this search process to improve the efficiency of object search.
The strategy for this accelerated function execution is Inline Cache, or IC for short. Next, let's take a look at how V8 uses IC to speed up the execution efficiency of the function loadX.
What is inline caching?
During the execution of the function, V8 will observe the key intermediate data on some call sites (CallSite) in the function, and then cache the data. When the function is executed again next time, V8 can directly use the intermediate data to save In order to obtain these data again, V8 uses IC to effectively improve the execution efficiency of some repetitive codes.
IC will maintain a feedback vector (FeedBack Vector) for each function. The feedback vector records some key intermediate data during the execution of the function.
For example, the following function:
function loadX(o) {
o.y = 4
return o.x
}
When V8 executes this function, it will judge that oy = 4 and return ox are the call site (CallSite), because they use objects and attributes, then V8 will in the feedback vector of loadX function for each call Click to allocate a slot. Each slot includes the slot index, the type of the slot, the state of the slot, the address of the hidden class (map), and the offset of the attribute.
For example, the two call points in the above function use the object o, then the map attributes in the two slots of the feedback vector also point to the same hidden class, so the map addresses of the two slots are the same.
When V8 calls the loadX function again, such as when the return ox statement in the loadX function is executed, it will find the offset of the x attribute in the corresponding slot, and then V8 can directly go to the memory to get the attribute value of ox Up. This greatly improves the execution efficiency of V8.
Polymorphism and superstate
By caching the basic information in the execution process, the efficiency of the next execution of the function can be improved.
But this has a premise, that is, the shape of the object is fixed when it is executed multiple times. If the shape of the object is not fixed, what will V8 do?
Let's adjust the code of the loadX function above, the adjusted code is as follows:
function loadX(o) {
return o.x
}
var o = { x: 1,y:3}
var o1 = { x: 3, y:6,z:4}
for (var i = 0; i < 90000; i++) {
loadX(o)
loadX(o1)
}
We can see that the shapes of objects o and o1 are different, which means that the hidden classes that V8 creates for them are also different.
Faced with this situation, V8 will choose to also record the new hidden class in the feedback vector, and at the same time record the offset of the attribute value. At this time, the first slot in the feedback vector contains two hidden classes and Offset
When V8 executes loadX again, it will also look up the feedback vector table. At this time, two hidden classes are recorded in the slot. At this time, V8 needs to do one extra thing, compare this new hidden class with the two hidden classes in the first slot one by one, if the same is found, then use the offset of the hidden class. What if there is not the same? Also add new information to the first slot of the feedback vector.
Therefore, a slot of a feedback vector can contain multiple hidden information:
The slot contains only 1 hidden class, we call this state monomorphic;The slot contains 2 to 4 hidden classes, which is called polymorphic;
There are more than 4 hidden classes in the slot, and this state is called a superstate (magamorphic).
Because polymorphism has a comparison link, the execution efficiency of polymorphism or superstate is definitely lower than that of single state.The performance of single state is better than polymorphism and super state, so we need to avoid polymorphism and super state a little bit.
Finally, I would like to emphasize that although the hidden classes and ICs we analyzed can improve the execution speed of the codeBut in actual projects, there are many factors that affect execution performance.
It is crucial to find out those bottlenecks that affect performance.
You don’t need to pay too much attention to micro-optimization, and you don’t need to worry too much about whether your code breaks the mechanism of hidden classes or IC.
Because compared to other performance bottlenecks, their impact on efficiency may be negligible.
Thinking Question### Three-level heading
Observe the following two pieces of code:
let data = [1, 2, 3, 4]
data.forEach((item) => console.log(item.toString())
let data = ['1', 2, '3', 4]
data.forEach((item) => console.log(item.toString())
Which of these two pieces of code do you think is highly efficient and why? You are welcome to share the discussion with me in the message area.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。