This article is participating in node.js advanced technology essay, click to see more details

V8 is an open source JavaScript engine developed by Google. It is used in Chrome, Node.js and other environments to compile JS code into assembly code corresponding to different CPUS (Intel, ARM, MIPS, etc.).

This article will help you understand the workflow of the V8 engine and thus learn how to write JS code that is easier to optimize.

JavaScript is an interpreted language

The first thing you need to understand about the V8 browser is that JavaScript is an interpreted language.

In order for the computer hardware to understand the source code, we need to convert it into binary instructions (machine code).

We divide high-level languages into compiled languages (static languages) and interpreted languages (dynamic languages) according to the timing of transformation.

Compiled languages (static languages)

Convert all source code into binary instructions at a time in advance to generate an executable program. Such as C language, C++, Golang, etc. The conversion tool becomes the compiler.

Interpreted languages (dynamic languages)

Compile as you execute, converting as much source code as you need, without generating an executable. For example, Python, JavaScript, PHP, Shell, MATLAB, etc., use conversion tools called interpreters.

Cross-platform programming languages

Compiled languages are generally not cross-platform, meaning they cannot switch between different operating systems.

Compared to compiled languages, interpreted languages are almost always cross-platform. Interpreted languages are cross-platform because of the interpreter middle layer. The interpreter translates the same source code into different machine code on different platforms, and the interpreter helps mask the differences between platforms.

Java and C# are a bit of an oddity. They are half-compiled, half-interpreted languages in which the source code is converted into an intermediate file (bytecode file), which is then taken to the virtual machine for execution. Java led the way, with its intent to be cross-platform and efficient; C# was a late follower, but C# stayed on Windows and had little success on other platforms.

conclusion

See the difference between compiled and interpreted languages

type The principle of advantages disadvantages
Compiled language All source code is converted at once into machine code (in the form of executable files) for execution on a particular platform (Windows, Linux, etc.) through a specialized compiler. After compiling once, it can be run without the compiler and run efficiently. Poor portability and flexibility.
Interpretive language A special interpreter temporarily converts part of the source code to platform-specific machine code as needed. Good cross-platform, through different interpreters, the same source code will be interpreted into different platform machine code. It is inefficient to convert while executing.

What does a JavaScript engine do?

The function of JavaScirpt engine is to compile JS code into assembly code corresponding to different CPUS (Intel, ARM, MIPS, etc.).

The JavaScript engine is also responsible for executing code, allocating memory, and garbage collection, rather than just compiling code.

Mainstream JS engines

At present, the mainstream JS engine has the following:

  • V8 (Chrome/Opera/Edge)
  • SpiderMonkey (Firefox)
  • JavaScriptCore (Safari)
  • Chakra (IE)

One of the most popular is Google’s V8 engine. In addition to Chrome and other browsers, The V8 engine is also used in Node.js and Electron.

What is a V8 engine?

In 2008, the V8 engine opened source on the same day as Chrome, and V8 was implemented in C++.

The performance of the V8 engine continues to improve after continuous optimization. For the evolution of the V8 engine, please refer to the article “Understanding THE JS Engine”, which has a detailed diagram.

How does the V8 engine work?

How does the CURRENT V8 engine work?

The V8 engine is made up of many sub-modules, of which these four are the most important.

  • Parser: Converts JavaScript source code to Abstract Syntax Tree (AST)
  • Ignition: Interpreter converts the AST to Bytecode, interprets and executes the Bytecode; Gather information needed for TurboFan optimization compilation, such as the types of function arguments;
  • TurboFan: Compiler, the compiler that converts Bytecode into optimized assembly code using the type information gathered by Ignitio;
  • Orinoco: Garbage collector, the garbage collector module that collects memory space no longer needed by the program;

Parser and Ignition are responsible for compiling and executing V8, which is how compiled languages are executed, so why TurboFan?

What is JIT?

We need to look at Just in Time compilation first.

Before running C, C++, Java and other programs, need to compile, can not directly execute the source code; With JavaScript, however, you can execute the source code directly (for example, Node Server.js) by compiling and executing it at runtime, a method known as just-in-time compilation, or JIT compilation. Therefore, V8 is also a JIT compiler.

Systems implementing JIT compilers typically constantly analyze the code being executed and identify parts of the code where the acceleration gained from compilation or recompilation would outweigh the overhead of compiling the code.

JIT compilation is a combination of two traditional machine code translation methods, AOT and explain, combining the strengths and weaknesses of both. In general, JIT compilation combines the speed of compiling code with the flexibility of interpretation, the overhead of the interpreter, and the additional overhead of compilation rather than just interpretation.

Aot-ahead of time corresponds to just in time.

In addition to the V8 engine, Java Virtual Machine and PHP 8 also use JIT.

What is bytecode?

Ignition turns JavaScript into Bytecode first, rather than Machine Code, which the Machine can execute directly. Why do I need this step?

What is bytecode first?

Bytecode (English: Bytecode) usually refers to code that has been compiled, but is independent of specific machine code, and needs to be translated by an interpreter to become intermediate code in the machine code. Bytecodes are usually not readable like source code, but encoded sequences of numeric constants, references, instructions, and so on.

The advantages of bytecode can be summed up as:

  • No specific CPU architecture
  • Faster than the original high-level language to machine language

Bytecode is used to Compile Once and Run anywhere.

The JIT of the V8 engine

For the V8 engine, the JIT workflow looks like this:

After the JavaScript source code is parsed into the AST, the Ignition interpreter interprets the execution code and collects information (including the number of times the function is executed). If the number of lines is called multiple times, it can be identified as Hot Code, and run information is fed back to TurboFan, an optimized compiler, which optimizes and compiles the bytecode to generate optimized machine Code.

The following figure summarizes the process. For more details, watch the video in reference, or see the explanation below.

The Parser generates an abstract syntax tree

The parsing process of interpreter is divided into two stages: lexical analysis and grammatical analysis.

This part of the knowledge, if you have studied the principles of compilation will understand.

Post a website where you can see the AST structure: astexplorer.net/

One optimization of the V8 Parsing phase was Lazy Parsing, which simply means that functions that are not executed immediately are preparsing only with the pre-Parser (Parsing only valid syntax, Parsing function declarations, and determining line-count scopes, but not complete Parsing).

The interpreter Ignition converts to bytecode

An interpreter that converts the AST into bytecode and interprets the execution of bytecode.

Ignition also collects information needed to optimize TurboFan compilation, such as the type of function arguments.

The Node command provides a number of options for viewing the production of the V8 engine at various stages of its operation.

Let’s create a new experiment code.

// test.js
function load(obj){
    return obj.x;
}

load({x:4.a:7});
Copy the code

Run the following node command to print the bytecode generated by Ignition.

node --print-bytecode test.js
Copy the code

To put it simply, LdaNamedProperty is a loaded property and then Returm returns it.

The TurboFan compiler optimizes the code execution flow

TurboFan: Compiler, a compiler that converts bytecode into optimized machine code using the type information gathered by Ignitio;

Let’s go back to the picture above.

In the image above, the green line is TurboFan using information gathered by Ignition to convert bytecode identified as hot code into optimized machine code.

So when will it be optimized? It is divided into the following situations:

  • 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.
  • If the function is called multiple times, it might be recognized as hot code, and TurboFan compiles the bytecode into optimized machine code to improve performance if Ignition’s type information proves optimized compilation.

In the figure above, the red line is a “Deoptimize” process that converts TurboFan’s optimized machine code back into bytecode if it doesn’t work for the code that needs to be executed.

Look at the following example.

function add(x, y) {
return x + y;
}

add(1.2);
add(2.2);
add("1"."2");
Copy the code

The arguments to add are integers and then strings. The generated optimization machine code already assumes that the arguments to the Add function are integers, which is of course incorrect, and needs to be optimized.

We can print the TurboFan generated machine code by executing the node command below.

node --print-code --print-opt-code test.js
Copy the code

For examples of TurboFan optimizing and de-optimizing bytecode based on type, check out this video: Franziska Hinkelmann: JavaScript Engines – How Do They Even? | JSConf EU.

TurboFan optimizes by type, including Inlining and Escape Analysis. See this article for more on the JS engine.

Inlining is combining related functions. Such as:

function add(a, b) {
  return a + b
}
function foo() {
  return add(2.4)}Copy the code

After inlining optimization:

function fooAddInlined() {
  var a = 2
  var b = 4
  var addReturnValue = a + b
  return addReturnValue
}

// Since fooAddInlined values of A and B are fixed, it can be further optimized
function fooAddInlined() {
  return 6
}
Copy the code

Escape analysis is simply analyzing whether an object’s lifetime is limited to the current function, and if so, optimizing it. Such as:

function add(a, b){
  const obj = { x: a, y: b }
  return obj.x + obj.y
}
Copy the code

After optimization of escape analysis:

function add(a, b){
  const obj_x = a
  const obj_y = b
  return obj_x + obj_y
}
Copy the code

The overall process

Refer to the article

The difference between compiled and interpreted languages

Lesson 4: How does the V8 engine work?

Getting to know the V8 engine

Deep understanding of JS engines

How does V8 execute JavaScript code?

Just-in-time (JIT) compilation

Why JIT improves performance so much?

Video: Franziska Hinkelmann: JavaScript Engines – How Do They Even? | JSConf EU