0%

Javascript引擎-V8

JavaScirpt引擎将JS代码编译为不同CPU(Intel, ARM以及MIPS等)对应的汇编代码,还负责执行代码、分配内存以及垃圾回收。

比较著名的javascript引擎:

V8

V8是最流行的Javascript引擎,Chrome(市场占有率60%)与Node.js(JS后端编程的事实标准)都使用了V8引擎。

严格来讲,V8所生成的代码是汇编代码而非机器代码,但是V8相关的文档、博客以及其他资料都把V8生成的代码称作machine code。

以下是关于V8的粗浅认识,参考自其他博主。

结构

V8由许多子模块构成,其中最重要的四个:

  • Parser:负责将JavaScript源码转换为Abstract Syntax Tree (AST)
  • Ignition:interpreter,即解释器,负责将AST转换为Bytecode,解释执行Bytecode;同时收集TurboFan优化编译所需的信息,比如函数参数的类型;
  • TurboFan:compiler,即编译器,利用Ignitio所收集的类型信息,将Bytecode转换为优化的汇编代码;
  • Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收;
V8流程图

Parser将JS源码转换为AST,然后Ignition将AST转换为Bytecode,最后TurboFan将Bytecode转换为经过优化的Machine Code(实际上是汇编代码)。

  • 如果函数没有被调用,则V8不会去编译它。
  • 如果函数只被调用1次,则Ignition将其编译Bytecode就直接解释执行了。TurboFan不会进行优化编译,因为它需要Ignition收集函数执行时的类型信息。这就要求函数至少需要执行1次,TurboFan才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为热点函数,且Ignition收集的类型信息证明可以进行优化编译的话,这时TurboFan则会将Bytecode编译为Optimized Machine Code,以提高代码的执行性能。

对于JavaScript来说,我们可以直接执行源码(比如:node server.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为JIT。因此,V8也属于JIT编译器。

以下主要涉及到对解释器和编译器的粗浅认识。

Ignition:解释器

example:

1
2
3
4
5
6
7
8
9
function factorial(N) {
if (N === 1) {
return 1;
} else {
return N * factorial(N - 1);
}
}

factorial(10);

使用node命令(node版本为12.6.0)的--print-bytecode选项,可以打印出Ignition生成的Bytecode。

1
node --print-bytecode factorial.js

控制台输出的内容非常多,最后是factorial函数的Bytecode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[generated bytecode for function: factorial]
Parameter count 2
Register count 3
Frame size 24
18 E> 0x127cbbb5685e @ 0 : a5 StackCheck
27 S> 0x127cbbb5685f @ 1 : 0c 01 LdaSmi [1]
31 E> 0x127cbbb56861 @ 3 : 68 02 00 TestEqualStrict a0, [0]
0x127cbbb56864 @ 6 : 99 05 JumpIfFalse [5] (0x127cbbb56869 @ 11)
46 S> 0x127cbbb56866 @ 8 : 0c 01 LdaSmi [1]
55 S> 0x127cbbb56868 @ 10 : a9 Return
75 S> 0x127cbbb56869 @ 11 : 1b 04 LdaImmutableCurrentContextSlot [4]
0x127cbbb5686b @ 13 : 26 fa Star r1
0x127cbbb5686d @ 15 : 25 02 Ldar a0
95 E> 0x127cbbb5686f @ 17 : 41 01 02 SubSmi [1], [2]
0x127cbbb56872 @ 20 : 26 f9 Star r2
84 E> 0x127cbbb56874 @ 22 : 5d fa f9 03 CallUndefinedReceiver1 r1, r2, [3]
83 E> 0x127cbbb56878 @ 26 : 36 02 01 Mul a0, [1]
99 S> 0x127cbbb5687b @ 29 : a9 Return
Constant pool (size = 0)
Handler Table (size = 0)

可以看到,Bytecode并不是一般意义上的汇编代码,并没有对应CPU指令集,或者说对应的是一个抽象出来的汇编指令集,这样引入的中间层可以保证V8针对不同的CPU简化编译流程。

TurboFan:编译器

TurboFan根据类型信息来简化代码执行流程,它还会进行其他优化,比如减少冗余代码等更复杂的事情。

使用node命令的--print-code以及--print-opt-code选项,打印出TurboFan生成的汇编代码。

1
node --print-code --print-opt-code factorial.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kind = REGEXP
name = ^([A-Z][a-z]+)+$
compiler = unknown
address = 0x7fffd74f95e0

Instructions (size = 1263)
0x81451ec2040 0 e983020000 jmp 0x81451ec22c8 <+0x288>
0x81451ec2045 5 4883e904 REX.W subq rcx,0x4
0x81451ec2049 9 c701c0020000 movl [rcx],0x2c0
0x81451ec204f f 48a1a0040f0300000000 REX.W movq rax,(0x30f04a0)
0x81451ec2059 19 483bc8 REX.W cmpq rcx,rax
0x81451ec205c 1c 0f8705000000 ja 0x81451ec2067 <+0x27>
0x81451ec2062 22 e840040000 call 0x81451ec24a7 <+0x467>
0x81451ec2067 27 488d47ff REX.W leaq rax,[rdi-0x1]
0x81451ec206b 2b 483b45b8 REX.W cmpq rax,[rbp-0x48]
0x81451ec206f 2f 0f859a030000 jnz 0x81451ec240f <+0x3cf>
0x81451ec2075 35 48897db0 REX.W movq [rbp-0x50],rdi
0x81451ec2079 39 488b4590 REX.W movq rax,[rbp-0x70]
0x81451ec207d 3d 4883e904 REX.W subq rcx,0x4
0x81451ec2081 41 8901 movl [rcx],rax
0x81451ec2083 43 48c7459000000000 REX.W movq [rbp-0x70],0x0
0x81451ec208b 4b 4883e904 REX.W subq rcx,0x4
0x81451ec208f 4f c70191020000 movl [rcx],0x291
...

针对不同的CPU,生成的汇编代码也不相同。

当然,理解TurboFan最重要的是其如何优化所生成的汇编代码。

以下通过add函数来梳理整个过程。

1
2
3
4
5
6
7
8
function add(x, y) {
return x + y;
}

add(1, 2);
add(3, 4);
add(5, 6);
add("7", "8");

由于JS的变量是没有类型的,所以add函数的参数可以是任意类型:Number、String、Boolean等,这就意味着add函数可能是数字相加(V8还会区分整数和浮点数),可能是字符串拼接,也可能是其他更复杂的操作。如果直接编译的话,生成的代码比如会有很多if…else分支去先判断参数类型。

由于Ignition在执行add(1, 2)时,已经知道add函数的两个参数都是整数,那么TurboFan在编译Bytecode时,就可以假定add函数的参数是整数,这样可以极大地简化生成的汇编代码。

这样做也是有风险的,因为如果add函数参数不是整数,那么生成的汇编代码也没法执行,只能Deoptimize为Bytecode来执行。

但如果我们的JS代码中变量的类型变来变去,是会给V8引擎增加不少麻烦的,为了提高性能,我们可以尽量不要去改变变量的类型。