preface

The main reference for this article is v8.dev/docs.

Interpreted versus compiled languages

Interpretive language

JavaScript is an interpreted language, a scripting language, a dynamic language. The source code we write (a high-level language) can be executed on any machine that contains its interpreter (or virtual machine) without prior compilation in the eyes of the user. For example, JS runs in a browser and shell scripts run in a shell dialog box.

Compiled language

Compiled languages differ in that we need to manually compile our code ahead of time and output code that the operating system can run directly. Such as C, C + +.

summary

What we often call interpreted versus compiled is simply a crude description of whether the user needs to manually compile. Js does not need to be compiled manually, i.e. interpreted, while C needs to be compiled manually. This description is not specific or graphic enough. Java, for example, is generally said to be compiled because it needs to be compiled in advance by JavAC, but the compiled bytecode cannot be directly executed by the operating system, and requires the Jvm to interpret the execution.

The compiled version generates the source code directly into machine code, saves it on disk, reads it into memory during execution and can be reused repeatedly. Interpretive type is to interpret the source code, or intermediate code, the execution of the middle code is saved in memory.

Of course JS is the interpreted language, and the JS virtual machine is V8.

Looking back at V8 history

Ancient times

  • In 2008,V8andchromBring it to market together. According to the V8 team itself, compared to the competition at the time,V8Can be up to 10 times faster; Today (then 2017), it’s 10 times faster than in 2008. Because V8 compiled JS source code directly (full codegen), and read into memory, one step in place, simple and crude. So the first run and subsequent runs are fast, but the biggest problem is too much memory.

Optimized compiler

  • In 2010, the optimized compiler was introducedChrankshaftIn theATSAs the tree is compiled into machine code,ChrankshaftAn intermediate code is generated, independent of the underlying machine architecture (but not bytecode), and optimization and de-optimization are performed based on analysis of this intermediate code. Reduces the compilation pressure on the CPU during execution.
  • In 2015, another optimized compiler was introducedTurboFun, used for pseudo complementChrankshaftBecause the latter does not support new JS syntax (such as async/await). The underlying code is optimized and the compilation performance is improved.

Interpreters and bytecode

  • 2016 was the year that interpreters and bytecodes were finally introduced. With a small cost of execution speed points, the problem of large memory consumption is fundamentally solved. As stated in the V8 team documentation, the interpreter’s introduction has three main goals:
    1. Reduce memory problems (bytecode is much smaller than machine code)
    2. Faster startup speed
    3. By adding bytecode, the V8 architecture is layered and simplified.
  • In 2017, seven years of service were removed completelyChrankshaftCompiler (previously for compatibility with older architectures). Finally, the bytecode interpreter (Ignation+ compiler optimizer (Turbofun) to this day.

The build process

First to a section of JS source code, see from the beginning

var a = 1;
var b = 2;
var c = a + b;
Copy the code

Parse

1. Lexical analysis

Split statement to form key-value array

[
    Var c = a + b
    {
        "type": "Keyword"."value": "var"
    },
    {
        "type": "Identifier"."value": "c"
    },
    {
        "type": "Punctuator"."value": "="
    },
    {
        "type": "Identifier"."value": "a"
    },
    {
        "type": "Punctuator"."value": "+"
    },
    {
        "type": "Identifier"."value": "b"},]Copy the code

2. Grammatical analysis

The one-dimensional array without language structure is converted into a tree structure with certain syntax. For later traversal to obtain bytecode preparation.

{
  "body": [
    Var c = a + b
    {
      "type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
            "type": "Identifier"."name": "c"
          },
          "init": {
            "type": "BinaryExpression"."operator": "+"."left": {
              "type": "Identifier"."name": "a"
            },
            "right": {
              "type": "Identifier"."name": "b"}}}]."kind": "var"}}],Copy the code

Generate bytecode

1. Bytecode and machine code

Node –print — bytecode test.js > bytecode

usingnode --print test.js > code, we went tocode.jsTo view bytecode:

A simple comparison can be made:

  1. Bytecode takes up less memory than machine code, which is a big reason for its introduction
  2. Machine code faces the operating system directly and is more abstract than bytecode, strongly relating to the underlying machine CPU architecture, such as (AMD.X86, Balabala), so poor mobility; The bytecode is wrapped in a layer that shields the underlying machine from the V8 compilerTurboFun.

2. Generate an overview

Let’s take a look at the bytecode generation process as a whole

void BytecodeGenerator::VisitAddExpression( BinaryOperation* expr) {
  Register lhs = 
      VisitForRegisterValue(expr->left());
  VisitForAccumulatorValue(expr->right());
  builder() - >AddOperation(lhs);
}
Copy the code

It’s very simple: you iterate through the ATS binary tree of operations (lvalue relative to registers, rvalue relative to accumulators) and output the bytecode corresponding to each node. Let’s use bytecode as an example to illustrate the generation process.

  1. First access the left node of the ATS treeaAnd will be the value of1Put into accumulator, corresponding output:LdaSmi.Wide [1]
  2. Take the value from the accumulator and place it in the register, corresponding toStar r0
  3. To access the right node, repeat steps 1,2
  4. Register at this timer0.r1The location is saved1.2And then you start adding them up
  5. willr1The register value is put into the accumulator, corresponding toLdar r1
  6. willr0Is added to the value on the accumulator, where the value on the accumulator is3
  7. Finally, take out the value on the accumulator and save it in the registerr2on

Photo source:www.iteye.com/blog/rednax…

3. Generation process

Let’s take a closer look at what the bytecode generation process does. Main reference

Register allocation

As you can see, the top few lines of each bytecode function indicate the number of registers required by the current function,Register count 3. For example, in our bytecode above, applyr0.r1.r2To hold temporary variables in an expression evaluation and local variables in a function. In addition, if there is a closure that uses variables of the outer function, register space is allocated to the Context object.

So let’s talk a little bit more about scope chains next. Context chains.

Explain to perform

Generates bytecode that cannot be executed directly and needs to be handled by Iterpreter.

1. Ignation

Let’s start with V8’s interpreter, code name: Big Nation. Register-based, indirect linear scheduling interpreter.

To explain register-based:

Interpreters generally come in two types, stack-based (JVM) and register-based. The stack structure uses operands to push and pop specific stack arrays, so there is no need to follow the operands’ execution address (zero-address instruction). Register-based systems require the address to be followed by the operand, such as Star r0, which takes out the accumulator value and stores it in register R0.

// Java bytecode, a stack-based interpreter
iconst_1  
iconst_2  
iadd  
istore_0  

// js bytecode, register-based interpreter
LdaSmi [1]
Star r0
PushContext r1
Copy the code

Again, indirect linear scheduling:

The first is indirect, that is, not direct, sequential interpretation execution, but rather the execution of a bytecode interpreter, and the next bytecode execution function is called inside the interpreter function, one after the other. Linear, I guess, means that each bytecode instruction executes a bytecode handler linearly.

photo

void Interpreter::DoAdd(InterpreterAssembler* assembler) {
  Node* reg_index = assembler->BytecodeOperandReg(0);
  Node* lhs = assembler->LoadRegister(reg_index);
  Node* rhs = assembler->GetAccumulator(a); Node* result = AddStub::Generate(assembler, lhs, rhs);
  assembler->SetAccumulator(result);
  // The tail calls the next bytecode
  assembler->Dispatch(a); }Copy the code

2. Stack frame layout of interpreter:

In the explanation executes, the built-in InterpreterEntryTrampoline stub will create a suitable stack frame.

First, some of the parameters in the compiler’s Furbofun environment: caller PC, frame pointer, JSFunction, Context, bytecode array, bytecode offset

Second, space is allocated for registers needed in the bytecode function. Used for subsequent bytecode interpretation.

photo

conclusion

That concludes the v8 compilation section on the interpreter, and the Turbofun compilation optimization section will be covered in the next installment