文章

V8引擎(二)

V8

V8 引擎学习(二)

V8引擎(二)

前瞻

上一篇文章讲到,JavaScript 代码需要转换为机器代码,CPU 才能识别并运行

那么为什么需要对 JavaScript 代码进行编译以及编译后是如何运行的?

高级代码为什么需要先编译再执行

一切高级编程语言都需要和 CPU 进行交流

那么 CPU 是如何执行机器代码的?

为了完成复杂的任务,工程师们为 CPU 提供了一大堆指令,来实现各种功能,这一大堆指令成为指令集(Instructions),也就叫做机器语言

注意,CPU 只能识别二进制的指令

对于程序员来说,二进制指令集难以记忆,因此又将二进制指令集转换为人类可以识别和记忆的符号,这就是汇编指令集

1000100111011000 - 机器指令
mov ax,bx - 汇编指令

显然,CPU 不能识别汇编语言,需要经过下图的转换

image-20220924133438441

CPU 不只是只有一种架构,如果要使用机器代码或者汇编代码来实现一个功能,那么需要为每种架构的 CPU 编写特定的汇编代码。其次,在编写汇编代码时,还需要了解和处理器架构相关的硬件知识。显然,这太过繁琐了,因此需要一种来屏蔽计算机架构细节的语言,随之,高级编程语言应运而生。

和汇编语言一样,处理器也不能直接识别由高级语言所编写的代码,通常有两种方式来执行这些代码

  • 解释执行

    • 需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释中间代码,随即得到结果

      image-20220924134649199

  • 编译执行

    • 需要先将输入的源代码转换成中间代码,之后编译器再将中间代码编译成机器代码。

    • 通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码。

      image-20220924135407477

以上,就是计算机执行高级语言的两种基本方式:解释执行、编译执行

V8 怎么执行 JavaScript 代码的

V8 作为 JavaScript 虚拟机的一种,它是如何执行 JavaScript 代码的呢?

实际上,V8 并没有采用某种单一的技术,而是混合解释执行和编译执行这两种执行方式。这种把解释器和编译器混合使用的方式称之为 JIT(Just In Time) - 即时编译技术。

那么,V8 为什么会采用这种方式呢?

这不得不说,解释执行和编译执行各自的优点和缺点了。

解释执行

  • 启动速度快
  • 执行速度慢

编译执行

  • 启动速度慢
  • 执行速度快

如此,这就保证了 V8 引擎的高性能。

跟踪一段代码的运行

这一行代码交给 V8 引擎后产生的结果是怎么样的呢?

// test.js
var test = "licodeao";

首先,这行代码会被解析器结构化为 AST

使用 d8 指令(d8 —print-ast test.js)来看看会输出啥

[generating bytecode for function: ]
--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (000001EAC5CE43E8) (mode = VAR, assigned = true) "test"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 192
. . . INIT at 192
. . . . VAR PROXY unallocated (000001EAC5CE43E8) (mode = VAR, assigned = true) "test"
. . . . LITERAL "licodeao"

上面就是转换后的 AST,可以将其转换为一个图形树方便观察

image-20220924141723823

生成 AST 的同时,还会生成作用域

使用 d8 指令(d8 —print-scopes test.js)来看看作用域是啥样的

Global scope:
global { // (000002A331BC4198) (0, 202)
  // will be compiled
  // 1 stack slots
  // temporary vars:
  TEMPORARY .result;  // (000002A331BC4518) local[0]
  // local vars:
  VAR test;  // (000002A331BC43E8)
}

可以明显地看到,test 变量被添加到了全局作用域中

生成了 AST 和作用域后,就可以使用解释器(ignition)生成字节码

使用 d8 指令(d8 —print-bytecode test.js)来看看生成的字节码

[generated bytecode for function:  (0x03530824fa29 <SharedFunctionInfo>)]
Parameter count 1
Register count 3
Frame size 24
         000003530824FA9A @    0 : 12 00             LdaConstant [0]
         000003530824FA9C @    2 : 26 fa             Star r1
         000003530824FA9E @    4 : 27 fe f9          Mov <closure>, r2
         000003530824FAA1 @    7 : 61 37 01 fa 02    CallRuntime [DeclareGlobals], r1-r2
         000003530824FAA6 @   12 : 12 01             LdaConstant [1]
         000003530824FAA8 @   14 : 15 02 00          StaGlobal [2], [0]
         000003530824FAAB @   17 : 0d                LdaUndefined
         000003530824FAAC @   18 : aa                Return
Constant pool (size = 3)
000003530824FA65: [FixedArray] in OldSpace
 - map: 0x0353080404b1 <Map>
 - length: 3
           0: 0x03530824fa51 <FixedArray[1]>
           1: 0x03530824f9f1 <String[#8]: licodeao>
           2: 0x0353081c851d <String[#4]: test>
Handler Table (size = 0)
Source Position Table (size = 0)

生成字节码后,解释器就会解释执行这段字节码,如果重复执行了某段代码,监控器就会将其标注为 hot 函数,并提交给编译器进行优化执行

使用 d8 指令(d8 —trace-opt test.js)查看哪些代码被优化了

如果要查看哪些代码被反优化了

使用 d8 指令(d8 —trace-deopt test.js)查看哪些代码被反优化了

显然,由于测试代码过于简单,并没有触发 V8 的优化机制,所以就不演示了。

总结

时隔这么久,也是很惭愧了。希望以后能坚持在周末输出专栏文章吧~

  • 由于计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种方式
    • 解释执行
      • 启动速度快
      • 执行速度慢
    • 编译执行
      • 启动速度慢
      • 执行速度快
  • V8 采用JIT(即时编译)技术来执行 JavaScript 代码,这是一种权衡策略,即在启动过程中采用了解释执行的策略,如果某段代码的执行频率超过了一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。