JavaScirpt引擎将JS代码编译为不同CPU(Intel, ARM以及MIPS等)对应的汇编代码,还负责执行代码、分配内存以及垃圾回收。
比较著名的javascript引擎:
- V8 (Google)
- SpiderMonkey (Mozilla)
- JavaScriptCore (Apple)
- Chakra (Microsoft)
- IOT:duktape、JerryScript
- QuickJS与Hermes
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,垃圾回收模块,负责将程序不再需要的内存空间回收;
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 | function factorial(N) { |
使用node命令(node版本为12.6.0)的--print-bytecode
选项,可以打印出Ignition生成的Bytecode。
1 | node --print-bytecode factorial.js |
控制台输出的内容非常多,最后是factorial函数的Bytecode:
1 | [generated bytecode for function: factorial] |
可以看到,Bytecode并不是一般意义上的汇编代码,并没有对应CPU指令集,或者说对应的是一个抽象出来的汇编指令集,这样引入的中间层可以保证V8针对不同的CPU简化编译流程。
TurboFan:编译器
TurboFan根据类型信息来简化代码执行流程,它还会进行其他优化,比如减少冗余代码等更复杂的事情。
使用node命令的--print-code
以及--print-opt-code
选项,打印出TurboFan生成的汇编代码。
1 | node --print-code --print-opt-code factorial.js |
1 | kind = REGEXP |
针对不同的CPU,生成的汇编代码也不相同。
当然,理解TurboFan最重要的是其如何优化所生成的汇编代码。
以下通过add函数来梳理整个过程。
1 | function add(x, y) { |
由于JS的变量是没有类型的,所以add函数的参数可以是任意类型:Number、String、Boolean等,这就意味着add函数可能是数字相加(V8还会区分整数和浮点数),可能是字符串拼接,也可能是其他更复杂的操作。如果直接编译的话,生成的代码比如会有很多if…else分支去先判断参数类型。
由于Ignition在执行add(1, 2)
时,已经知道add函数的两个参数都是整数,那么TurboFan在编译Bytecode时,就可以假定add函数的参数是整数,这样可以极大地简化生成的汇编代码。
这样做也是有风险的,因为如果add函数参数不是整数,那么生成的汇编代码也没法执行,只能Deoptimize为Bytecode来执行。
但如果我们的JS代码中变量的类型变来变去,是会给V8引擎增加不少麻烦的,为了提高性能,我们可以尽量不要去改变变量的类型。
…