V8 简介
相信很多的朋友都听过前端界的一个著名定律,叫做 Atwood’s Law。2007 年,Jeff Atwood 提出 “所有可以用 JavaScript 编写的应用程序最终都会用 JavaScript 编写”。转眼 12 年过去,现在,我们的确可以看到,JavaScript 在浏览器端、服务端、桌面端、移动端、IoT 领域都发挥着作用。
另一方面,截至目前(2019-11-08),Chrome 在全平台的市场占有率已经达到 64.92%(数据来源:StatCounter)。作为 Chrome 的 JavaScript 引擎,V8 在 Chrome 扩大市场占有率方面也起到十分关键的作用。
作为最强大的 JavaScript 引擎之一,V8 同样是无处不在。在浏览器端,它支撑着 Chrome 以及众多 Chromium 内核的浏览器运行。在服务端,它是 Node.js 及 Deno 框架的执行环境。在桌面端和 IoT 领域,也同样有 V8 的一席之地。
V8 是使用 C++
编写的高性能 JavaScript
和 WebAssembly
引擎,支持包括我们熟悉的 ia32、x64、arm 在内的八种处理器架构。
V8 的重要部件
Ignition(基线编译器)
TurboFan(优化编译器)
Orinoco(垃圾回收器)
Liftoff(WebAssembly 基线编译器)
V8管道简介
早期 V8 执行管道由基线编译器 Full-Codegen 与优化编译器 CrankShaft 组成。
其中,基线编译器更注重编译速度,而优化编译器更注重编译后代码的执行速度。综合使用基线编译器和优化编译器,使 JavaScript 代码拥有更快的冷启动速度,在优化后拥有更快的执行速度。
这个架构存在诸多问题,例如,Crankshaft 只能优化 JavaScript 的一个子集;编译管道中层与层之间缺乏隔离,在某些情况下甚至需要同时为多个处理器架构编写汇编代码等等。
为了解决架构混乱和扩展困难的问题,经过多年演进,V8 目前形成了由解析器、基线编译器 Ignition 和优化编译器 TurboFan 组成的 JavaScript 执行管道。
在执行管道改进的过程中,通过引入 IR(Intermediate representation,中间表示),有效地提升了系统可扩展性,降低了关联模块的耦合度及系统的复杂度。
解析器与AST
解析代码需要时间,所以 JavaScript 引擎会尽可能避免完全解析源代码文件。另一方面,在一次用户访问中,页面中会有很多代码不会被执行到,比如,通过用户交互行为触发的动作。
正因为如此,所有主流浏览器都实现了惰性解析(Lazy Parsing)。解析器不必为每个函数生成 AST(Abstract Syntax tree,抽象语法树),而是可以决定“预解析”(Pre-parsing)或“完全解析”它所遇到的函数。
预解析会检查源代码的语法并抛出语法错误,但不会解析函数中变量的作用域或生成 AST。完全解析则将分析函数体并生成源代码对应的 AST 数据结构。相比正常解析,预解析的速度快了 2 倍。
生成 AST 主要经过两个阶段:分词和语义分析。AST旨在通过一种结构化的树形数据结构来描述源代码的具体语法组成,常用于语法检查(静态代码分析)、代码混淆、代码优化等。
生成AST可以通过在线工具AST Explorer进行体验。
基线编译器Ignition与字节码
V8 引入 JIT(Just In Time,即时编译)技术,通过 Ignition 基线编译器快速生成字节码进行执行。
字节码是机器码的抽象。如果字节码的设计与物理 CPU 的计算模型相同,那么将字节码编译成机器代码就会更加容易。
和之前的基线编译器 Full-Codegen 相比,Ignition 生成的是体积更小的字节码(Full-Codegen 生成的是机器码)。字节码可以直接被优化编译器 TurboFan 用于生成图(TurboFan 对代码的优化基于图),避免优化编译器在优化代码时需要对 JavaScript 源代码重新进行解析。
优化编译器 Turbo 优化与去优化
编译器需要考虑的函数输入类型变化越少,生成的代码就越小、越快。
众所周知,JavaScript 是弱类型语言。ECMAScript 标准中有大量的多义性和类型判断,因此通过基线编译器生成的代码执行效率低下。
举个例子,+ 运算符的一个操作数就可能是整数、浮点数、字符串、布尔值以及其它的引用类型,更别提它们之间的各种组合(ECMA对于+的定义)。
但这并不意味着 JavaScript 代码没有办法被优化。对于特定的程序逻辑,其接收的参数往往是类型固定的。正因为如此,V8 引入了类型反馈技术。在进行运算的时候,V8 使用类型反馈对所有参数进行动态检查。
简单来说,对于重复执行的代码,如果多次执行都传入类型相同的参数,那么 V8 会假设之后每一次执行的参数类型也是相同的,并对代码进行优化。优化后的代码中会保留基本的类型检查。如果之后的每次执行参数类型未改变,V8 将一直执行优化过的代码。而当之后某一次执行时传入的参数类型发生变化时,V8 将会“撤销”之前的优化操作,这一步称为“去优化”(Deoptimization)。
代码缓存
在用户访问相同的页面,并且该页面关联的脚本文件没有任何改动的情况下,代码缓存技术会让 JavaScript 的加载和执行变得更快。
代码缓存被分为 cold、warm、hot 三个等级。
用户首次请求 JS 文件时(即 cold run),Chrome 将下载该文件并将其提供给 V8 进行编译,并将该文件缓存到磁盘中。
当用户第二次请求这个 JS 文件时(即 warm run),Chrome 将从浏览器缓存中获取该文件,并将其再次交给 V8 进行编译。在 warm run 阶段编译完成后,编译的代码会被反序列化,作为元数据附加到缓存的脚本文件中。
当用户第三次请求这个 JS 文件时(即 hot run),Chrome 从缓存中获取文件和元数据,并将两者交给 V8。V8 将跳过编译阶段,直接反序列化元数据。