4

走进chrome内心,了解V8引擎是如何工作的

 3 years ago
source link: https://segmentfault.com/a/1190000040331440
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作为一个前端程序员,每天上班的第一件事就是打开电脑,不由自主的点开chrome浏览器,或是摸会儿鱼或是立马进入工作状态。接下来浏览器窗口就会陪伴着你度过一天的时光,正常到七八点钟,晚点就九十点钟,再晚点就陪你跨过一天,时刻关注着你的工作。作为一个忠诚陪伴你的伙伴,你扪心自问,你有认真的了解过它是如何工作的吗?你有走进过它的内心世界吗?

如果你也好奇过,那么请收看这期的《走进chrome内心,了解V8引擎是如何工作的》。

V8是什么

在深入了解一件事物之前,首先要知道它是什么。

V8是一个由Google开源的采用C++编写的高性能JavaScriptWebAssembly引擎,应用在 ChromeNode.js等中。它实现了ECMAScriptWebAssembly,运行在Windows 7及以上、macOS 10.12+以及使用x64、IA-32、ARMMIPS处理器的Linux系统上。 V8可以独立运行,也可以嵌入到任何C++应用程序中。

接下来我们来关心关心它如何诞生的,以及为什么叫这个名字。

V8最初是由Lars Bak团队开发的,以汽车的V8发动机(有八个气缸的V型发动机)进行命名,预示着这将是一款性能极高的JavaScript引擎,在2008年9月2号chrome一同开源发布。

为什么需要V8

我们写的JavaScript代码最终是要在机器中被执行的,但机器无法直接识别这些高级语言。需要经过一系列的处理,将高级语言转换成机器可以识别的的指令,也就是二进制码,交给机器执行。这中间的转换过程就是V8的具体工作。

接下来我们就来详细的了解一下。

首先来看一下V8的内部组成。V8的内部有很多模块,其中最重要的4个如下:

  • Parser: 解析器,负责将源代码解析成AST
  • Ignition: 解释器,负责将AST转换成字节码并执行,同时会标记热点代码
  • TurboFan: 编译器,负责将热点代码编译成机器码并执行
  • Orinoco: 垃圾回收器,负责进行内存空间回收

V8工作流程

以下是V8中几个重要模块的具体工作流程图。我们逐个分析。

V8工作流程.png

Parser解析器

Parser解析器负责将源代码转换成抽象语法树AST。在转换过程中有两个重要的阶段:词法分析(Lexical Analysis)语法分析(Syntax Analysis)

也称为分词,是将字符串形式的代码转换为标记(token)序列的过程。这里的token是一个字符串,是构成源代码的最小单位,类似于英语中单词。词法分析也可以理解成将英文字母组合成单词的过程。词法分析过程中不会关心单词之间的关系。比如:词法分析过程中能够将括号标记成token,但并不会校验括号是否匹配。

JavaScript中的token主要包含以下几种:

关键字:var、let、const等

标识符:没有被引号括起来的连续字符,可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些内置常量

运算符: +、-、 *、/ 等

数字:像十六进制,十进制,八进制以及科学表达式等

字符串:变量的值等

空格:连续的空格,换行,缩进等

注释:行注释或块注释都是一个不可拆分的最小语法单元

标点:大括号、小括号、分号、冒号等

以下是const a = 'hello world'经过esprima词法分析后生成的tokens

[
    {
        "type": "Keyword",
        "value": "const"
    },
    {
        "type": "Identifier",
        "value": "a"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "String",
        "value": "'hello world'"
    }
]

语法分心是将词法分析产生的token按照某种给定的形式文法转换成AST的过程。也就是把单词组合成句子的过程。在转换过程中会验证语法,语法如果有错的话,会抛出语法错误。

上述const a = 'hello world'经过语法分析后生成的AST如下:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": "hello world",
            "raw": "'hello world'"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

经过Parser解析器生成的AST将交由Ignition解释器进行处理。

Ignition解释器

Ignition解释器负责将AST转换成字节码(Bytecode)并执行。字节码是介于AST和机器码之间的一种代码,与特定类型的机器代码无关,需要通过解释器转换成机器码才可以执行。

看到这里想必大家都有疑惑,既然字节码也需要转换成机器码才能运行,那一开始为什么不直接将AST转换成机器码直接运行呢?转换成机器码直接运行速度肯定更快,那为什么还要加一个中间过程呢?

其实在V85.9版本之前是没有字节码的,而是直接将JS代码编译成机器码并将机器码存储到内存中,这样就占用了大量的内存,而早期的手机内存都不高,过度的占用会导致手机性能大大的下降;而且直接编译成机器码导致编译时间长,启动速度慢;再者直接将JS代码转换成机器码需要针对不同的CPU架构编写不同的指令集,复杂度很高。

5.9版本以后引入了字节码,可以解决上述内存占用大、启动时间长、代码复杂度高这几个问题。

接下来我们来看看Ignition是如何将AST转换成字节码的。

下图是Ignition解释器的工作流程图。AST需要先通过字节码生成器,再经过一系列的优化之后才能生成字节码。

ignation.png

其中的优化包括:

  • Register Optimizer:主要是避免寄存器不必要的加载和存储
  • Peephole Optimizer:寻找字节码中可以复用的部分,并进行合并
  • Dead-code Elimination: 删除无用的代码,减少字节码的大小

将代码转换成字节码后就可以通过解释器执行了。Ignition在执行的过程中,会监视代码的执行情况并记录执行信息,如函数的执行次数、每次执行函数时所传的参数等。

当同一段代码被执行多次,就会被标记成热点代码。热点代码会交给TurboFan编译器进行处理。

TurboFan编译器

TurboFan拿到Ignition标记的热点代码后,会先进行优化处理,然后将优化后字节码编译成更高效的机器码存储起来。下次再次执行相同代码时,会直接执行相应的机器码,这样就在很大程度上提升了代码的执行效率。

当一段代码不再是热点代码后,TurboFan会进行去优化的过程,将优化编译后的机器码还原成字节码,将代码的执行权利交还给Ignition

现在我们来看一看具体的执行过程。

sum += arr[i]为例,由于JS是动态类型的语言,每次的sumarr[i]都有可能是不同的类型,在执行这段代码时,Ignition每次都会检查sumarr[i]的数据类型。当发现同样的代码被执行了多次时,就将其标记为热点代码,交给TurboFan

TurboFan在执行时,如果每次都判断sumarr[i]的数据类型是很浪费时间的。因此在优化时,会根据之前的几次执行确定sumarr[i]的数据类型,将其编译成机器码。下次再执行时,省去了判断数据类型的过程。

但如果在后续的执行过程中,arr[i]的数据类型发生了改变,之前生成的机器码就不满足要求了,TurboFan会把之前生成的机器码丢弃,将执行权利再交给Ignition,完成去优化的过程。

热点代码:
image.png

优化前:
image.png

优化后:
image.png

现在我们来总结一下V8的执行过程:

  1. 源代码经过Parser解析器,经过词法分析和语法分析生成AST
  2. AST经过Ignition解释器生成字节码并执行
  3. 在执行过程中,如果发现热点代码,将热点代码交给TurboFan编译器生成机器码并执行
  4. 如果热点代码不再满足要求,进行去优化处理

这种字节码与解释器和编译器结合的技术,就是我们通常所说的即时编译(JIT)。

本文并没有介绍垃圾回收器OrinocoV8的垃圾回收机制可以单独用一篇文章来详细介绍,我们下期再见。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK