深入浅出Electron:原理、工程与实践
上QQ阅读APP看书,第一时间看更新

5.2 V8脚本执行原理

目前开发人员使用的编程语言主要分为两大类:一类是编译执行的语言;另一类是解释执行的语言。

像C、C++和汇编语言都是编译执行的语言,使用这些语言开发程序的开发者,需要把自己编写的代码编译成二进制文件,这些针对不同机器生成的二进制文件可以直接在目标机器上运行,但为不同机器生成的二进制文件不能在其他的机器上运行。

像Java、C#等编程语言是解释执行的语言,使用这些语言开发程序的开发者,需要把自己编写的代码编译成字节码文件,这些字节码文件不能直接在任何机器上运行,目标机器必须安装某个虚拟机或运行时,比如Java虚拟机JVM或C#的运行时.NET Framework,才能执行这类文件,但这些字节码文件在任何机器的虚拟机上都可以正确执行。

我们把第一种方式称为编译执行,第二种方式称为解释执行,第一种方式的特点是启动速度较慢,执行速度较快,调试不是很方便,针对不同的目标机器要生成不同的可执行文件;第二种方式的特点是启动速度较快,执行速度较慢,调试方便,只需生成一套字节码文件就可以在不同的机器上正确执行。

很显然JavaScript属于解释执行的编程语言,但V8引擎却是身兼多能的运行时,它既有编译执行的特点又有解释执行的特点,下面我们就来具体分析一下。

在V8开始执行一段JavaScript脚本前,它做了如下三个事情:

1)初始化内存中的堆栈结构。V8有自己的堆空间和栈空间设计,代码运行期产生的数据都是存储在这些空间内的,所以要提前完成初始化工作。

2)初始化全局环境。这个工作包含一些全局变量、工具函数的初始化任务。

3)初始化消息循环。这个工作包含V8的消息驱动器和消息队列的初始化任务。

做完这些工作之后V8就可以执行JavaScript源码了,需要说明的是,我们经过webpack或Rollup处理过的JavaScript源码,甚至被javascript-obfuscator混淆过的JavaScript源码,对于V8引擎来说都没什么两样,就是一个很长的字符串。

V8首先把这些字符串转化为抽象语法树(AST abstract syntax code),我们来分析一段简单的JavaScript代码:

let param = 'v8'
console.log('hello ${param}')

这段代码生成的抽象语法树如下所示:

094-1

从上面的信息可以看出,第8行使用LET定义了变量param,第12~13行为param变量赋值字符串v8,第17~20行开始调用console对象的log方法,第21~24行使用模板字符串拼接字符串,最后一行返回执行结果。

在生成抽象语法树之后,V8会为程序中的变量生成作用域,如下所示:

global { // (000001952B7A00F8) (0, 47)
  // will be compiled
  // 1 stack slots
  // 3 heap slots
  // temporary vars:
  TEMPORARY .result;  // (000001952B7A0698) local[0]
  // local vars:
  LET param;  // (000001952B7A0348) context[2]
  // dynamic vars:
  DYNAMIC_GLOBAL console;  // (000001952B7A0768) never assigned
}

从上面的信息可以看出,V8在执行这段代码时用到了三个变量:一个是临时变量.result,用于存储返回值;一个是用户使用LET关键字声明的param变量;一个是动态全局变量console。所有这些变量都包裹在全局作用域中。

有了抽象语法树之后,V8接下来会把抽象语法树转换为字节码,如下所示:

Parameter count 1
Register count 4
Frame size 32
         000002440824FABE @   0 : 12 00           LdaConstant [0]
         000002440824FAC0 @   2 : 1d 02           StaCurrentContextSlot [2]
         000002440824FAC2 @   4 : 13 01 00        LdaGlobal [1], [0]
         000002440824FAC5 @   7 : 26 f9           Star r2
         000002440824FAC7 @   9 : 28 f9 02 02     LdaNamedProperty r2, [2], [2]
         000002440824FACB @  13 : 26 fa           Star r1
         000002440824FACD @  15 : 12 03           LdaConstant [3]
         000002440824FACF @  17 : 26 f8           Star r3
         000002440824FAD1 @  19 : 1a 02           LdaCurrentContextSlot [2]
         000002440824FAD3 @  21 : 78              ToString
         000002440824FAD4 @  22 : 34 f8 04        Add r3, [4]
         000002440824FAD7 @  25 : 26 f8           Star r3
         000002440824FAD9 @  27 : 59 fa f9 f8 05  CallProperty1 r1, r2, r3, [5]
         000002440824FADE @  32 : 26 fb           Star r0
         000002440824FAE0 @  34 : aa              Return
Constant pool (size = 4)
000002440824FA85: [FixedArray] in OldSpace
 - map: 0x0244080404b1 <Map>
 - length: 4
           0: 0x02440824fa05 <String[#2]: v8>
           1: 0x0244081c6971 <String[#7]: console>
           2: 0x0244081c69e5 <String[#3]: log>
           3: 0x02440824fa15 <String[#6]: hello >
Handler Table (size = 0)
Source Position Table (size = 0)

这段字节码中类似Add、Star、Lda ***等指令都是在操作寄存器。

我们知道通常有两种架构的解释器,基于栈的解释器和基于寄存器的解释器。基于栈的解释器会将一些中间数据存放到栈中,而基于寄存器的解释器会将一些中间数据存放到寄存器中。由于采用了不同的模式,所以字节码的指令形式是不同的。

很显然,V8引擎是基于寄存器的解释器。

如果V8的解释器发现某段代码将会被反复执行(很可能代码中存在循环体或某个函数被反复调用),V8的监控器就会将这段代码标记为热点代码,并提交给编译器优化执行。

我们修改一下前面的测试代码,以促使V8对其优化,如下所示:

let x = 0
for (let i = 0; i < 666666; i++) {
  x = 66+66
}

这是一段无意义的代码,for循环内不断地执行一个加法运算,当V8解释执行这段代码时,发现这个加法运算是可以优化的,就会对其进行优化,优化的过程信息如下所示:

[marking 0x028b0824fab9 <JSFunction (sfi = 0000028B0824FA2D)> for optimized recompilation, reason: small function]
[compiling method 0x028b0824fab9 <JSFunction (sfi = 0000028B0824FA2D)> using TurboFan OSR]
[optimizing 0x028b0824fab9 <JSFunction (sfi = 0000028B0824FA2D)> - took 1.026, 1.095, 0.064 ms]

这个过程信息解释了优化的原因:small function,这个for循环是一个小函数,为了减少函数调用的开销,所以对其做优化操作。using TurboFan OSR是V8使用的优化引擎。最后一行信息代表着优化所耗费的时间。

V8会针对不同类型的代码执行不同的优化手段,最为人所称道的就是直接把字节码编译为二进制代码,由于二进制代码具备执行速度快的特点,所以这也是V8引擎高效的原因之一。

回到本节的开头,我们可以说V8是一种内嵌了编译执行能力的JavaScript解释器。它是复杂的,但也是高效的,因为它同时具备两种能力——编译执行能力和解释执行能力。

关于V8执行过程中的这些中间信息我是怎么拿到的,后面还会有更详细的介绍。