likes
comments
collection
share

Vol.2 V8引擎 - JS执行原理

作者站长头像
站长
· 阅读数 1

最近在调用WASM,但是在开始聊WASM前,想要先复习一下V8引擎基础的运行原理。

1. 我们的计算机是怎么读懂我们的代码的

最底层的语言 - 机器码

一般来说,所有的计算机能读懂的只有一种语言 - 机器码。并且不同的计算机因为技术架构(x86,ARM等)不同,能读懂的机器码也不同。机器码是二进制的代码,简单来说就是通过0/1表示关/开。因为所有的计算机硬件其实就是电路系统,电流的通断可以使用0和1来表示。通过电路系统中的开和关,就能控制硬件的行为。这就是计算机帝国唯一通行语言 - 机器码

1.1 程序员会的语言 - 编程语言

一般来说,我们到了一个新的国家,只要学习他们的语言就能深入交流了。但是问题在于这个国家的语言非常晦涩难懂。可以参考以下例子:

// 这是编译出来的机器码(转换成为十六进制)
B8 04 00 00 02
BB 01 00 00 00
B9 23 01 00 00
BA 0D 00 00 00
CD 80
B8 01 00 00 00
CD 80

从上面的示例可以看出机器码的开发成本和可读性十分差。就算有大神能学会了机器码编写程序,但是维护这套代码成本实在太高,开发并维护一套大型系统的机器码已经超出人力的极限了。因此诞生出了我们经常听到或用到的更高级的语言,如:C,C++,JAVA,Python,JS等

// 这是一个上面机器码编译前的C++代码
#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!";
    return 0;
}

从上方示例代码,就算没有学习过C++,但是只要学习过英文,可能也能大概猜出来具体表达的是什么,代码的可读性大大提高了。

注意上文说的“高级”只是在架构上层级更高,并不是代表编程语言的优秀程度。

1.2. 翻译官 - 编译器

那么这时问题来了,我们用编程语言编写的程序,计算机他不可能读懂,他们只认机器码,这时怎么办呢?这时程序员想要游历计算机帝国,就得带上“翻译官”了。翻译官通常有两种工作方式:第一种就是在程序员写完代码的时候,他们就会把写好的代码翻译成为各种架构的机器码,然后在计算机中就能执行了。第二种就是,在计算机里面运行一个虚拟机/容器,对应的代码只在虚拟机/容器里面执行,由虚拟机/容器在具体运行的时候再转换成机器码来和计算机进行对接。这个转换的过程就叫“编译”。第一种很好理解,就是程序员把编写好的指令,提前通过翻译官翻译好,直接丢给计算机执行就好了。第二种就相当于程序员带上了个同声传译耳机,一边读命令,这个耳机一边转换成机器码并告诉计算机这时需要做什么。两种方式各有利弊:

  • 方式一:
    • 优点:机器直接执行机器码,效率更高。
    • 缺点:需要预先知道代码运行的平台,并且为那个平台翻译代码。
    • 代表语言:C++
  • 方式二:
    • 优点:更好的平台兼容性,只需要对应平台支持了编程语言的虚拟机,那么就能使用你的程序。
    • 缺点:由于是运行过程中经过了一层转换,因此执行效率相对较低。
    • 代表语言:JAVA,JavaScript

而我们JS使用的就是第二种方式,对应的虚拟机/容器就是我们准备要介绍的V8引擎。

1.3. 虚拟机/容器语言 - 字节码 / IR

在介绍V8引擎工作原理之前,这里还需要补充一下。一般来说我们的编程源码,也不能直接在对应的虚拟机中执行,这些源码会在程序员编写完成程序后,编译生成特殊的二进制中间代码。这些中间代码一般是字节码格式,也称为IR(Intermediate Representation)。字节码的优势:

  • 效率更高:更低级,更贴近机器码,能在虚拟机中快速解析执行。并且在编译时已经被优化过。
  • 跨平台:更好的兼容多个平台。
  • 安全性:字节码运行在虚拟机中,不直接对接硬件,所有的行为由虚拟机管控。
  • 易于送交Just-In-Time(JIT)编译:一些虚拟机,例如Java虚拟机(JVM)和 JS的引擎(V8),使用JIT编译技术实时地将字节码转化为本地机器代码,以提高代码的执行速度。

我们熟悉的wasm也是一种字节码。

最后我们通过一张图来总结一下:Vol.2 V8引擎 - JS执行原理

上述方式二没有以JS为例子,是因为V8其实接收的直接是JS,所以有一点区别。

2. V8是怎么工作的

V8的工作流程我们可以直接用图来表示:Vol.2 V8引擎 - JS执行原理接下来我们分析每一步V8都做了些什么。

2.1 parser

parser主要是做的是通过词法分析语法分析将源码转换成AST(虚拟语法树)。

2.1.1 词法分析与语法分析

词法分析

词法分析会将js源码转换成一个一个tokens。这些token可以理解为最小的js词汇,比如变量、操作符、括号等。以以下简单的代码为例:

if (x > 10) {
  y = 20;
} else {
  y = 30;
}

经过词法分析后会得到:

"if", "(", "x", ">", "10", ")", "{", "y", "=", "20", ";", "}", "else", "{", "y", "=", "30", ";", "}"

语法分析

语法分析会将这些 tokens 组合成一个树状的数据结构,这便是抽象语法树AST,在这个结构中,每个节点都代表源代码中的一个语法结构。经过语法分析后生成最终的AST

IfStatement {
  test: BinaryExpression {
    operator: '>',
    left: Identifier {
      name: 'x',
    },
    right: Literal {
      value: 10,
    },
  },
  consequent: BlockStatement {
    body: [
      ExpressionStatement {
        expression: AssignmentExpression {
          operator: '=',
          left: Identifier {
            name: 'y',
          },
          right: Literal {
            value: 20,
          },
        },
      },
    ],
  },
  alternate: BlockStatement {
    body: [
      ExpressionStatement {
        expression: AssignmentExpression {
          operator: '=',
          left: Identifier {
            name: 'y',
          },
          right: Literal {
            value: 30,
          },
        },
      },
    ],
  },
}

可以看到,这个AST清晰地反映了源代码的语法结构,甚至包括了条件判断、表达式计算、赋值操作等多种语法元素。

2.2 Ignition - 解释器

  • Igition主要的作用就是遍历上方生成的AST转化为字节码的过程。
  • 生成字节码后,Ignition会立即开始解释执行这些字节码,无需将其转化为机器码。

当然Ignition其实就像一个虚拟机,它底层在运行的时候,依然还是需要把相关的指令转换成机器码与机器交互的,只不过这个过程是在这个虚拟机运行时执行的。

以下为字节码示例:Vol.2 V8引擎 - JS执行原理 :::info Ignition除了负责执行字节码外,在运行过程中还会收集运行时信息(包括代码一共运行了多少次、如何运行的等信息),Ignition会把多次执行的代码标记为热点代码,这些代码有什么用,请往下看。 :::

2.3 TurboFan

经过上面的描述,可能大家有点疑惑,都已经可以在虚拟机直接执行字节码了,那为什么还会有一个TurboFan呢?它的作用是什么?

2.3.1 虚拟机的利与弊

在V8引擎出来之前,JS引擎执行代码基本上都是到Ignition这一步就结束了。但是随着技术的发展,人们发现了一个问题:解释器/虚拟机启动速度确实很快,但是由于是边解释边执行的,那么对到频繁使用的代码,或者对到循环代码会存在多次解析的情况,从而导致运行速度变慢,降低了JS的执行效率。这里可以以上述翻译的例子,我们想象一个翻译器,它能做到同声传译(用户一边讲它一边翻译)。由于用户讲什么是实时的,它无法预先知道用户讲什么,那么所以对到相同的话语它只能一次又一次的重新翻译,避免出现翻译错误的情况。

2.3.2 V8优化 - JIT

为了解决以上的问题,V8引擎引入了编译器,也就是我们这一节讲的TurboFan。 :::info 这个编译器会把上面解释器(Ignition)标识的热点代码进行编译,编译为机器码,并且对这部分机器码进行一定的代码优化,等下次V8引擎再执行相同的代码时,会直接跳过解释器(Ignition),采用这段编译后的机器码直接执行,从而提升效率。 ::: 以上的这个过程,就是V8引擎的JIT(Just In Time)优化。当然JIT优化还有更多的原理细节,这里就不一一解释,有兴趣的同学可以查询详细的资料。

2.3.3 编译器的利与弊

小孩子才做选择,V8引擎全都要(解释器,编译器)。对到不常用的代码直接可以使用解释器执行即可,对到常用的热点代码,就转换为机器码,随时拿出执行,这样能一定程度上提升了JS的执行效率。当然,记录这些代码的信息,其实是需要有一定的开销的,这也是使用编译器的弊端。