# 一、开始
本文介绍了编译、解释、动静态语言等基本概念,以及 V8 引擎的基本流程。
# 二、编译与解释
二进制指令就是机器码
- 编译:将源代码一次性转换成目标代码的过程。执行编译过程的程序叫编译器(Compiler)。
- 解释:将源代码逐条转换成目标代码,同时逐条运行的过程。执行解释过程的程序叫解释器(Interpreter)。解释器一般来说就是 vm,vm 有两种,一种是基于堆栈,一种是基于寄存器。
编译过程大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件等五个步骤,期间涉及到复杂的算法和硬件架构。解释器与此类似。
# 三、静态语言与动态语言
高级语言按照执行方式的不同,可分为静态语言和动态语言。
- 静态语言:使用编译执行的语言,如 C、C++、Golang 等。使用编译器一次性生成目标代码,“一次编译,无限次运行”,程序运行速度更快。编译型语言一般是不能跨平台的,也就是不能在不同的操作系统之间随意切换。
- 动态语言:使用解释执行的语言,如 Python、Javascript、PHP 等。执行过程中需要源代码,只要存在解释器,源代码可以在任何操作系统上运行,可移植性好,“一次编写,到处运行”。
解释型语言之所以能够跨平台,是因为有了解释器这个中间层。在不同的平台下,解释器会将相同的源代码转换成不同的机器码,解释器帮助我们屏蔽了不同平台之间的差异。
java 和 C# 是一种比较奇葩的存在,它们是半编译半解释型的语言,源代码需要先转换成一种中间文件(字节码文件),然后再将中间文件拿到虚拟机中执行。Java 引领了这种风潮,它的初衷是在跨平台的同时兼顾执行效率;C# 是后来的跟随者,但是 C# 一直止步于 Windows 平台,在其它平台鲜有作为。
总结一下:
类型 | 原理 | 优点 | 缺点 |
---|---|---|---|
编译型语言 | 通过专门的编译器,将所有源代码一次性转换成特定平台(Windows、Linux 等)执行的机器码(以可执行文件的形式存在)。 | 编译一次后,脱离了编译器也可以运行,并且运行效率高。 | 可移植性差,不够灵活。 |
解释型语言 | 由专门的解释器,根据需要将部分源代码临时转换成特定平台的机器码。 | 跨平台性好,通过不同的解释器,将相同的源代码解释成不同平台下的机器码。 | 一边执行一边转换,效率很低。 |
# 四、V8引擎
Javascript 是解释型语言,那么 V8 引擎就对应着解释器。但是 V8 引擎为了提高 JS 的运行效率,会提前编译。
也就是 V8 引擎包括两个阶段:编译、执行,编译阶段指 V8 将 JavaScript 转换为字节码或者二进制机器码,执行阶段指解释器解释执行字节码,或者 CPU 直接执行二进制机器码。
# 1. JIT
V8 引擎同时采用了解释执行和编译执行这两种方式,也就是在运行时进行编译,这种方式称为 JIT (Just in Time
) 即时编译。
V8 在执行 JavaScript 源码时,会先通过解析器将源码解析成 AST,解释器会将 AST 转化为字节码,一边解释一遍执行。
解释器同时会记录某一代码片段的执行次数,如果执行次数超过了某个阈值,这段代码便会被标记为热代码(Hot Code
),同时将运行信息反馈给优化编译器 TurboFan,TurboFan 根据反馈信息,会优化并编译字节码,最后生成优化的机器码。
# 2. Parser 生成抽象语法树
Parser 生成 AST 抽象语法树过程包括语法分析、词法分析,和 Babel 等工具差不多。
生成 AST 中的一个优化是惰性解析(Lazy Parsing),因为源码在执行前如果全部完全解析的话,不仅执行时间过长,而且会消耗更多的内存。
惰性解析就是指如果遇到并不是立即执行的函数,只会对其进行预解析(
Pre-Parser
),当函数被调用时,才会对其完全解析。 预解析时,只会验证函数的语法是否有效、解析函数声明以及确定函数作用域,并不会生成 AST,这项工作由Pre-Parser
预解析器完成。
# 3. Ignition 生成字节码
字节码是机器码的抽象,可以看作是小型的构建块。相比机器码,字节码不仅占用内存少,而且生成字节码的时间很快,提升了启动速度。
另外,字节码与特定类型的机器码无关,通过解释器将字节码转换为机器码后才可以执行,这样也使得 V8 更加方便的移植到不同的 CPU 架构。
可以通过如下命令,查看 JavaScript 代码生成的字节码。
node --print-bytecode index.js
注意,解释器执行字节码前,还是会将字节码转为机器码,因为计算机只识别机器码。
# 4. TurboFan
Ignition 执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码),然后把这段代码发送给 编译器TurboFan。 然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。 另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到 Ignition。
TurboFan 做的优化包括内联(inlining
)和逃逸分析(Escape Analysis
)。
内联就是将相关联的函数进行合并,减少运行时间。比如:
function add(a, b) {
return a + b
}
function foo() {
return add(2, 4)
}
内联处理后:
function fooAddInlined() {
var a = 2
var b = 4
var addReturnValue = a + b
return addReturnValue
}
// 因为 fooAddInlined 中 a 和 b 的值都是确定的,所以可以进一步优化
function fooAddInlined() {
return 6
}
逃逸分析就是分析对象的生命周期是否仅限于当前函数,如果是的话会对其进行优化。比如:
function add(a, b){
const obj = { x: a, y: b }
return obj.x + obj.y
}
会处理成:
function add(a, b){
const obj_x = a
const obj_y = b
return obj_x + obj_y
}