preface

I don’t know if you’ve ever thought about it, but how does the JavaScript code we write get recognized and executed by the computer? What exactly is the process?Some of you may already know that Js is run by a Js engine, so

  • What is a Js engine?
  • How does a Js engine compile, execute and optimize Js code?

There are many types of JAVASCRIPT engines, such as V8 for Chrome, JavaScriptCore for Webkit and Hermes for React Native. Today we are going to analyze how the more mainstream V8 engine runs Js.

V8 engine

Before introducing the V8 concept, let’s review the programming language. Programming languages can be divided into machine language, assembly language, high-level language.

  • Machine language: Binary zeros and ones are hard for humans to remember, and compatibility between different CPU platforms is also important.
  • Assembly language: Replace binary instructions with easier to remember abbreviated English identifiers, but still requires hardware knowledge.

  • High-level languages: More simple and abstract and without regard to hardware, but require more complex and time-consuming translation processes to be performed.

At this point, we know that a high-level language must be converted to machine language before it can be executed by a computer, and the more advanced the language, the longer the conversion.High-level language can be divided into interpretive language, compiled language.

  • Compiled languages: require the compiler to compile once, and the compiled file can be executed multiple times. Such as C++, C language.
  • Interpreted languages: do not need to be compiled in advance and are interpreted as they are executed by the interpreter. Fast to start but slow to execute.

We know that JavaScript is a high-level and dynamically typed language, so we don’t need to care about the type of a variable when we define it, and we can change the type of the variable at will. In statically typed languages like C++, we have to declare the variable’s type and assign it the correct value. JavaScript is also an interpreted language because it does not provide enough information in advance for the compiler to compile lower-level machine code, as C++ does. It can only collect type information at run time, and then compile and execute from that information.

This means that in order for JavaScript to be executed by a computer, it needs a program that can quickly parse and execute JavaScript scripts, which is what we usually call a JavaScript engine. V8 is Google’s open source high-performance Javascript and WebAssembly engine written in C++. For Google Chrome (Google’s open source browser) and Node.js, etc.

How does the CPU execute machine instructions?

After converting a high-level language into a machine language, how does the CPU perform? Take a piece of C code as an example:

int main(a)

{

    int x = 1;

    int y = 2;

    int z = x + y;

    return z;

}}
Copy the code

Let’s take a look at what this code looks like when translated into machine language. The figure below shows binary machine code in hexadecimal on the left, assembly code in the middle, and instruction meanings on the right.

The process by which the CPU executes machine instructions

  • First, the program is loaded into memory before execution.
  • The system writes the address of the first instruction in the binary code into a PC register.

  • The CPU fetches instructions from memory based on addresses in PC registers.
  • Updates the address of the next instruction to the PC register.

  • Analyze the current fetch instructions and identify the different types of instructions, as well as the various methods of obtaining operands.

  • Load instruction: Copies the contents of a specified length from memory to the general register and overwrites the original contents of the register.

  • Store instruction: Copies the contents of a register to a location in memory and overwrites the original contents of that location in memory.

The %ecx following the MOVL instruction in the figure above is the register address. -8(% RBP) is the memory address. The function of this instruction is to copy the value of the register into memory.

  • Update instruction: Copy the contents of two registers into ALU, or a block of register and a block of memory into ALU, ALU adds two words, stores the result in one of the registers, and overwrites the contents of the register.

.

  • After the command is executed, the next CPU clock cycle starts.

The assembly line for V8 engines

Let’s take a macro look at how V8 executes JavaScript code and then examine each step.

  • Initialize the base environment;
  • Parsing source code to generate AST and scope;
  • Bytecode generation based on AST and scope;
  • Interprets execution of bytecode; Listen for hotspot code;
  • .

A complete analysis of how a piece of JavaScript code is executed

1. Initialize the base environment

V8 cannot execute Js code without the host environment, which can be a browser or Node.js. The following figure shows the structure of the browser, in which the rendering engine is usually referred to as the browser kernel, which includes the network module, Js interpreter and so on. When a render process is opened, a runtime environment is initialized for V8.The runtime environment provides V8 with heap space, stack space, global execution context, message loop system, host objects, host apis, and more. The core of V8 is the implementation of the ECMAScript standard, as well as the garbage collector.

2. Parse the source code to generate AST and scope

With the base environment in place, it’s time to submit the JavaScript code to V8 to execute. First, V8 receives the JavaScript source code to execute, but it’s just a bunch of strings to V8, and V8 doesn’t directly understand what the string means; it needs to structure it.



function add(x, y) {

  var z = x+y

  return z

}

console.log(add(1.2))
Copy the code

For example, V8 first parses the source code into the following abstract syntax tree AST:



[generating bytecode for function: add] -AST ---

FUNC at12.KIND0.LITERAL ID1.SUSPEND COUNT0.NAME "add".PARAMS.VAR (0x7fa7bf8048e8) (mode = VAR, assigned = false)"x"..VAR (0x7fa7bf804990) (mode = VAR, assigned = false)"y".DECLS.VARIABLE (0x7fa7bf8048e8) (mode = VAR, assigned = false)"x"..VARIABLE (0x7fa7bf804990) (mode = VAR, assigned = false)"y"..VARIABLE (0x7fa7bf804a38) (mode = VAR, assigned = false)"z".BLOCK NOCOMPLETIONS at- 1.EXPRESSION STATEMENT at31...INIT at31....VAR PROXY local[0] (0x7fa7bf804a38) (mode = VAR, assigned = false)"z"....ADD at32.....VAR PROXY parameter[0] (0x7fa7bf8048e8) (mode = VAR, assigned = false)"x".....VAR PROXY parameter[1] (0x7fa7bf804990) (mode = VAR, assigned = false)"y".RETURN at37.VAR PROXY local[0] (0x7fa7bf804a38) (mode = VAR, assigned = false)"z"
Copy the code

V8 generates the AST along with the scope of the add function:



Global scope:

function add (x, y) { // (0x7f9ed7849468) (12, 47)

  // will be compiled

  // 1 stack slots

  // local vars:

  VAR y;  // (0x7f9ed7849790) parameter[1], never assigned

  VAR z;  // (0x7f9ed7849838) local[0], never assigned

  VAR x;  // (0x7f9ed78496e8) parameter[0], never assigned

}
Copy the code

During parsing, all variables and function parameters declared in the function body are put into scope. The default value is undefined if they are normal variables, and refers to the actual function object if they are function declarations. During execution, variables in scope point to the corresponding data in the heap and stack.

Generate bytecode according to AST and scope

Once the scope and AST are generated, V8 can use them to generate bytecode. The AST is then passed as input to the BytecodeGenerator, which is part of the Ignition interpreter and is used to generate the bytecode in terms of functions.



[generated bytecode for function: add (0x079e0824fdc1 <SharedFunctionInfo add>)]

Parameter count 3

Register count 2

Frame size 16

         0x79e0824ff7a@ 0:a7                StackCheck

         0x79e0824ff7b@ 1:25 02Ldar a1

         0x79e0824ff7d@3:34 03 00Add a0, [0]

         0x79e0824ff80@ 6:26fb             Star r0

         0x79e0824ff82@ 8:0c 02             LdaSmi [2]

         0x79e0824ff84@ 10:26fa             Star r1

         0x79e0824ff86@ 12:25fb             Ldar r0

         0x79e0824ff88@ 14:ab                Return

Constant pool (size = 0)

Handler Table (size = 0)

Source Position Table (size = 0)
Copy the code

4. Explain the execution of bytecode

Similar to how the CPU executes binary machine code: it uses an area of memory to hold bytecode; The universal register is used to store some intermediate data. The PC register is used to point to the next bytecode to execute; The top of the stack register is used to point to the current top of the stack.

  • The StackCheck bytecode instruction checks to see if the stack has reached the upper limit of overflow.
  • Ldar means to load the value from the register into the accumulator.

  • Add means that the register loads the value and adds it to the value in the accumulator, and then puts the result into the accumulator again.

  • Star means to save the value from the accumulator to a register.

  • Return ends execution of the current function and returns control to the caller. The value returned is the value in the accumulator.

Real-time compilation

When the interpreter Ignition executes the bytecode, if it finds HotSpot code — for example, a piece of code that has been executed multiple times — the TurboFan compiler in the background compiles the bytecode into efficient machine code. When the optimized code is executed again, it only needs to execute the compiled machine code, which greatly improves the efficiency of the code execution. This technique of pairing bytecode with interpreter and compiler is called just-in-time compilation (JIT).

V8 optimization strategy

Let’s take a look at some of the optimizations V8 has made to speed up parsing and executing Js. Due to space constraints, only 5 optimization points are presented here.

1. Reintroduce bytecode

Early V8 teams decided that bytecode-by-bytecode was inefficient, so they compiled JavaScript code directly into machine code. The problem with this is that it takes a long time to compile, and the resulting binary machine code takes up a lot of memory.Using bytecode sacrifices a bit of execution efficiency, but saves memory and reduces compile time. In addition, bytecode reduces the complexity of V8 code, making it easier to port V8 to different CPU architecture platforms. This is because it is easier for compilers to uniformly convert bytecode to binaries for different platforms than it is for compilers to write binaries for different CPU systems.

2. Delayed resolution

As you can see from V8’s compilation process, executing JavaScript code in V8 involves two stages: compilation and execution.

  • Compilation process: The stage where V8 converts JavaScript code to bytecode, or binary machine code.
  • Execution phase: the phase in which the interpreter interprets the execution of bytecode, or the CPU executes binary machine code directly.

V8 does not parse all JavaScript into intermediate code at once for two reasons:

  • If all the JavaScript code is parsed and compiled at once, too much code can increase compilation time, which can seriously affect the speed of the first execution of the JavaScript code and make the user feel sluggish.
  • Second, parsed bytecode and compiled machine code are stored in memory, and if you parse and compile all your JavaScript code at once, the intermediate code and machine code will always be in memory.

Deferred parsing means that if the parser encounters a function declaration during parsing, it skips the code inside the function and does not generate the AST and bytecode for it.

3. Hide classes

We can combine this code to see how the hidden class works:

let point = {x:100.y:200}
Copy the code

When V8 executes this code, it first creates a hidden class for the Point object. In V8, the hidden class is also called a Map, and each object has a map attribute whose value points to the hidden class in memory. The hidden class describes the layout of the attributes of an object. It mainly contains the names of the attributes and the offsets corresponding to each attribute. For example, the hidden class of the Point object contains the x and Y attributes, with the offsets of x being 4 and Y 8.With hidden classes, when V8 accesses an attribute in an object, it first looks for the offset of the attribute relative to its object in the hidden class. With the offset and the attribute type, V8 can fetch the corresponding attribute value directly from memory without going through a series of look-up procedures. This greatly improves the efficiency of FINDING objects in V8.

4. Fast and slow properties

When we enter the following code on the console:

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]}`)}Copy the code

The printout looks like this:

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
Copy the code

This result occurs because the ECMAScript specification defines that numeric attributes should be sorted in ascending order by index value size, and string attributes in ascending order by order of creation.

  • The numeric properties are called sort properties, which in V8 are called Elements.
  • String properties are called general properties, or properties in V8.

Let’s run some code to see how the structure of an object changes in memory when the number of attributes in the object changes.



function Foo(property_num,element_num) {

    // Add sort attributes

    for (let i = 0; i < element_num; i++) {

        this[i] = `element${i}`

    }

    // Add general attributes

    for (let i = 0; i < property_num; i++) {

        let ppt = `property${i}`

        this[ppt] = ppt

    }

}

var bar = new Foo(10.10)
Copy the code

Switch Chrome Developer Tools to the Memory TAB and click on the small circle to the left to capture a Memory snapshot of the above code, as shown below:Adjust the number of object attributes created to 20

var bar2 = new Foo(20.10)
Copy the code

Bottom line: When there are too many attributes in an object, or when there are repeated operations to add or remove attributes, V8 demots the linear storage mode (fast attributes) to the nonlinear dictionary storage mode (slow attributes), which slows down look-up times but improves the speed of modifying attributes of the object.

5. Inline caching

Let’s look at one more piece of code like this.

function loadX(o) {

    o.y = 4

    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)

}
Copy the code

The usual process for V8 to get O.x is this: look for the hidden class of object O, look for the offset of the x attribute through the hidden class, and then get the value of the attribute based on the offset. LoadX is executed repeatedly in this code, so the process of getting O.x needs to be executed repeatedly. To improve object search efficiency. V8 implements the strategy of using Inline Cache, or IC for short. IC maintains a FeedBack Vector for each function, which records key intermediate data during the function’s execution. The data is then cached so that V8 can use the intermediate data directly the next time the function is executed, saving the process of retrieving the data again. V8 assigns a Slot to each call point in the feedback vector, such as o.y = 4 and return O.x, which are callsites because they use objects and attributes. Each slot contains the slot index, the slot type, the slot state, the address of the hidden class map, and the offset of the attribute. For example, in this function, the two call points use the object O. Then the map attribute in both slots of the feedback vector also points to the same hidden class, so the map address of the two slots is the same.The inline caching strategy improves the efficiency of the next execution of the function, but only if the shape of the object is fixed for multiple executions. If the shape of the object is not fixed, it means that V8 creates different hidden classes for them. In this case, V8 will choose to record the new hidden class in the feedback vector as well as the offset of the attribute value. In this case, a slot in the feedback vector will contain multiple hidden classes and offsets. If there are more than four, V8 will use a hash table structure to store them. At this point, my sharing is over. If there are any shortcomings, welcome your criticism.

Refer to the link

www.cnblogs.com/nickchen121…

v8.dev/docs

Juejin. Cn/post / 684490…

Time.geekbang.org/column/arti…

www.jianshu.com/p/e4a75cb6f…

Welcome to “Byte front-end ByteFE” resume delivery email “[email protected]