The origin of the V8 engine

Definition of engine: car engine vs JS engine

Automobile engine: The conversion of fuel chemical energy into mechanical energy of pistons

JS engine: Compiles JavaScript source code into machine code that can be executed by the computer and executes it

  • JScript(IE6,IE7, IE8)
  • Chakra(IE9,IE10, IE11, IE Edge)
  • SpiderMonkey(Firefox)
  • JavaScriptCore(Safari)
  • V8(Chrome)

JS V8 engine name from the same name V8 sports car series engine engine, on behalf of horsepower, speed. 8 cylinders divided into 2 groups of 4, arranged in a V-shape.”

Apple’s open source WebKit rendering engine

  • V8 engine is a JavaScript engine implementation originally designed by some language experts, acquired by Google, and subsequently open-source by Google.
  • V8 is developed in C++, compiles JavaScript to native machine code (V8 later introduced bytecode) before running it, and uses methods such as inline caching to improve performance, rather than other JavaScript engines converting to bytecode or interpreting execution.
  • V8 supports a wide range of operating systems, such as Windows, Linux and Android, as well as other hardware architectures. It is portable and cross-platform.

Node.js = v8 + built-in modules (mostly written in JS)

2. The working process of V8 engine

After April 2017:

When V8 compiles JavaScript code, the Parser generates an abstract syntax tree. Syntax trees are tree representations of the syntactic structure of JavaScript code. The interpreter Ignition generates bytecode from the syntax tree. TurboFan is V8’s optimized compiler, TurboFan generates bytecode optimized machine code. Understand V8 bytecode

  • If the function is not called, V8 does not compile it.
  • If the function is called only once, Ignition compiles it and Bytecode explains the execution directly. TurboFan does not optimize compilation because it requires Ignition to gather information about the type of function it executes. This requires that the function be executed at least once before TurboFan can be optimized for compilation.
  • Bytecode is compiled as Optimized Machine Code by TurboFan if the function is called multiple times and it is likely to be recognized as a hot function. TurboFan will compile Bytecode as Optimized Machine Code to improve execution performance when Ignition collects type information that justifies Optimized compilation.

Optimized Machine Code will be restored to Bytecode. This process is called Deoptimization. That’s because Ignition can collect the wrong information, such as an integer argument to the Add function that then turns into a string. Optimized Machine Code has been generated to assume that the arguments to the Add function are integers, which of course is incorrect, so Deoptimization is required.

Q: Why did V8 reintroduce bytecode in 2017?

There are three main motivations:

  • Reducing the amount of memory taken up by machine code, i.e. sacrificing time for space
  • Improve the startup speed of code
  • Refactoring v8 code to reduce v8 code complexity

Explanation:

Because machine code takes up a lot of space, v8 cannot compile all js code into machine code and cache it if machine code takes up too much memory. And even if you can fully cache, such a cache takes up a lot of memory, disk space, exit Chrome to open the serialization, deserialization of the cache will take a long time, time, space costs are unacceptable. As shown below, JS source code is compiled directly into machine code:

So V8’s next best thing is to compile only the outermost JS code, shown in green in the example above. The internal code (shown in yellow and red) is not compiled, but when is it compiled? V8 deferred compilation until it was first called. Another disadvantage of time is that code must be parsed multiple times — once for green code, once again for yellow code (when new Person is called), and once again for red code (when doWork() is called). Therefore, if your JS code’s closures cover n layers, they will eventually be parsed by V8 at least n times.

With the introduction of bytecode, the space problem can be alleviated. Bytecode can be much more compact than machine code by properly designing its encoding. Here’s a comparison of the code footprint after V8 introduced Ignition bytecode, and you can see that it actually went down.

“Bytecode is an abstraction of machine code” — bytecode interpretation execution is faster than JS source code compilation to machine code execution, and bytecode takes up less memory than machine code and is compiled ahead of time, so caching bytecode can achieve both speed and reduce memory footprint.

[Portal] V8 Ignition: The JS Engine’s Connection to bytecode

3. Three-point performance optimization of V8 engine

3.1 Hidden Classes

A hidden class is similar to a pointer to a C language that represents the address of the store. Creating a new hidden class opens up a new storage address.

function Person(name, age) {
    this.name = name;
    this.age = age;
}

var xs = new Person("xszi", 1000);
var ve = new Person("v8"19); xs.email ="[email protected]";
xs.job = "coder";

ve.job = "driver";
ve.email = "[email protected]";
Copy the code

When the Person object is initialized, a C0 hidden class is initially created with no attributes. Then, when the constructor function is called, the engine generates transition hidden classes C1 and C2 with the offset of the attribute recorded inside the hidden class as the attribute is added. Transitional hidden classes exist so that hidden classes can be shared across multiple objects.

Here, the xs and ve objects use the same constructor, so they share the same hidden class C2. Later, although the attributes of Job and Email are added to both xs and VE objects, different hidden classes are generated due to different initialization sequences.

The generated hidden classes are different for objects in different initialization sequences. Therefore, in the actual development process, we should try to ensure that the sequence of attribute initialization is consistent, so that the generated hidden classes can be shared, hidden classes exist in memory, so as to speed up the object access operation. At the same time, try to initialize all object members in the constructor, reducing the generation of hidden classes.

3.2 Incline Caching

The previous section covered hidden classes, but it’s not enough to have them; the engine needs to find them during execution. Inline Caching is a technique used to optimize the run-time lookup of objects and their properties.

【 QUESTION 】 What is the relationship between hidden classes and inline caching?

Whenever a method is called on a particular object, the V8 engine must find the object’s hidden class to determine the offset to access a particular property. When the same method is successfully called to the same hidden class twice, V8 skips the lookup of the hidden class and jumps directly to the property’s memory address using the offset stored in the inline cache. For all future calls of this method, the V8 engine assumes that the hidden class has not changed and jumps directly to the memory address of the particular property using the offset previously found and stored. This greatly improves the speed of execution.

The following figure shows an example of objects A and B sharing the hidden class Shape, with x and y having different offsets.

Let’s look at what inline caching is.

Suppose we call a function getX with an object {x: ‘a’} as an argument to get the property value. From the above analysis, we can see that this object has a hidden class Shape with the attribute ‘x’. Shape stores the offset and attribute of the attribute x. JSFunction ‘getX’ in green is the bytecode generated by getX. Where the get_by_id directive loads the attribute ‘x’ from argument (arg1) and stores the result into loc0. In addition, the engine embeds Shape into the get_by_id directive, which consists of two uninitialized slots.

When you first execute this function, the get_by_id directive looks for the attribute ‘x’ and finds that the value is stored at offset 0.

The inline cache embedded in the get_by_id directive remembers to find the Shape and offset of the attribute:

For subsequent runs, you only need to compare the inline cache and, if it is the same as before, simply load the value of the memory offset. Specifically, if the JavaScript engine sees an object with a previously recorded Shape, it doesn’t need to access property information at all and can skip expensive property information lookup altogether. This is much faster than looking at the properties each time.

Inline cache:

Optimizing dynamic JavaScript with inline caches

Shapes and inline caching

3.3 Garbage Collection (GC)

In V8, all JavaScript objects are divided by heap. The heap is divided into new generation memory space and old generation memory space, and different garbage collection algorithms are used for garbage collection respectively. In addition, garbage collection is a progressive garbage collection mechanism

  • The Scavenge Algorithm, a new generation of recycling algorithms.

If the Form space is a live object, it will be copied To the To space. If the space is not a live object, the space occupied by the From space will be released directly. After the replication, the roles of the From space and To space will be swapped.

The advantage of the algorithm is that only the surviving objects are copied, and only a small part of the new generation of surviving objects are copied, so the efficiency is excellent. The disadvantage is that only half of the heap memory is used, so it cannot be applied to all garbage collection on a large scale. However, the short life cycle of new generation objects is very suitable for this algorithm.

V8 schematic of memory

The first promotion condition

The second condition for promotion

Question: Why is the limit set to 25%?

When the Scavenge is completed, the To space will become the From space, and subsequent memory allocation will be made in this space. If the ratio is too high, the subsequent memory allocation will be affected.

  • Old garbage collection algorithm — Mark-sweep & Mark-Compact

    The Scavenge algorithm is not used by older generations.

    • There are many living objects in old generation and the replication efficiency is very low.
    • Waste half the space

The principle behind the well-marked method is to traverse all old objects in the heap, mark living objects, and clear dead objects. The diagram below:

Because of the low efficiency of moving objects, Mark-Compact is not used in most cases. It is used only when there is insufficient space.

V8 also introduced other optimization methods, only three of which are known so far, but there will be time to fill in later.

4. To summarize

4.1 Advantages of V8 engine

  • Use the Ignition+ TurboFan combination, sacrificing time for space, to boost V8 performance
  • Use hidden classes and inline caching for fast data access
  • Different garbage collection methods are used for different types of memory space to improve the collection efficiency.

4.2 Front-end developer code optimization methods

  • Object properties are always instantiated in the same order so that hidden classes and subsequently optimized code can be shared, assigning all object properties in their constructors

  • Code that executes the same method repeatedly will run faster than code that executes many different methods just once (due to inline caching), so you can generalize and encapsulate some common methods.

  • Deoptimization is very performance-intensive; reduce Deoptimization operations and use canonical variable types such as Typescript.

5. Reference materials

  • Why is the V8 so fast?
  • Understand V8 bytecode
  • Why is JavaScript fast?
  • How does the V8 engine work?
  • V8 Ignition: The JS Engine’s Connection to bytecode
  • Optimizing dynamic JavaScript with inline caches
  • Shapes and inline caching
  • Getting to know the V8 engine
  • JavaScript V8 engine
  • V8 engine optimization mechanism for hidden classes and inline caching
  • Node.js by Ling Pu