前端V8引擎编译器和解释器

Posted 程序员思语

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端V8引擎编译器和解释器相关的知识,希望对你有一定的参考价值。

阅读时间 | 15分钟

面对大厂,熟练掌握JS和深厚的算法基础是必备的,而网络、浏览器相关的知识点也是重点考察的范围,今天开始逐一分析V8引擎、Webkit、JSCore等等知识点并深入各个细节

前言

前端工具和框架的自身更新速度非常块,比如一周前(10月5号凌晨 Vue3.0 Pre-Alpha 版本已发布),而且还不断有新的出现。要想追赶上前端工具和框架的更新速度,你就需要抓住那些本质的知识,然后才能更加轻松地理解这些上层应用。比如接下来要介绍的 V8 执行机制,可以帮助我们从底层了解 javascript,也能帮助你深入理解语言转换器 Babel、语法检查工具 ESLint、前端框架 Vue 和 React 的一些底层实现机制。
众所周知V8引擎使用 C++ 开发,在运行 JavaScript 之前,相比其它的 JavaScript 的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32,x86-64,ARM,or MIPS CPUs),并且使用了如 内联缓存( inline caching )等方法来提高性能。有了这些功能, JavaScript 程序在 V8 引擎下的运行速度媲美二进制程序。V8支持众多操作系统,如 windows、linux、android 等,也支持其他硬件架构,如 IA32,X64,ARM 等,具有很好的可移植和跨平台特性。
下面将从 编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、字节码
(Bytecode)、即时编译器(JIT)等开始入手 V8引擎。

一、编译器和解释器

之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。
按照大学课本的解释就是,编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言( 注意看文章结尾的特别注明)。
前端V8引擎(一)编译器和解释器

从图中你可以看出这二者的执行流程,大致可阐述为如下:

  1. 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生
    成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果
    编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,
    那么编译器就会抛出异常,最后的二进制文件也不会生成成功。

  2. 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生
    成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来
    执行程序、输出结果。

二、V8 是如何执行 JavaScript 代码的

V8 在执行过程中既有解释器 Ignition,又有编译器 TurboFan,那么它们是如何配合去执行一段 JavaScript 代码的呢?

前端V8引擎(一)编译器和解释器


答案:生成抽象语法树(AST)和执行上下文将源代码转换为抽象语法树,并生成执行上下文。
下面重点讲解下 抽象语法树(AST ),看看什么是 AST 以及 AST 的生成过程是怎样的,(在Vue的 tamplate 那篇文章也有介绍 AST,本质是一样的)。
高级语言是开发者可以理解的语言,但是让编译器或者解释器来理解就非常困难了。对于编译器或者解释器来说,它们可以理解的就是 AST 了。所以无论你使用的是解释型语言还是编译型语言,在编译过程中,它们都会生成一个 AST。这和渲染引擎将 html 格式文件转换为计算机可以理解的 DOM 树的情况类似。
关于AST之前介绍过,这里只简单举个例子:
var myName = " 测试V8引擎 "
function foo(){
  return 200;
}
myName = "V8引擎"
foo()
这段代码经过 JavaScript-AST  站点处理后,生成的 AST 结构如下:

前端V8引擎(一)编译器和解释器

从图中可以看出,AST 的结构和代码的结构非常相似,其实你也可以把 AST 看成代码的结构化的表示,编译器或者解释器后续的工作都需要依赖于 AST,而不是源代码。
AST 是非常重要的一种数据结构,在很多项目中有着广泛的应用。其中最著名的一个项目是 Babel。Babel 是一个被广泛使用的代码转码器,可以将 ES6 代码转为 ES5 代码,这意味着你可以现在就用 ES6 编写程序,而不用担心现有环境是否支持 ES6。
Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。
除了 Babel 外,还有 ESLint 也使用 AST。ESLint 是一个用来检查 JavaScript 编写规范的插件,其检测流程也是需要将源码转换为 AST,然后再利用 AST 来检查代码规范化的问题。
现在你知道了什么是 AST 以及它的一些应用,那接下来我们再来看下 AST 是如何生成的。通常,生成 AST 需要经过两个阶段。
第一阶段是分词(tokenize),又称为词法分析,其作用是将一行行的源码拆解成一个个 token。本文中的 token(),是指 语法上不可能再分的、最小的单个字符或字符串。
第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。这就是 AST 的生成过程,先分词,再解析。有了 AST 后,那接下来 V8 就会生成该段代码的执行上下文。

三、生成字节码

有了 AST 和执行上下文后,那接下来的第二步,解释器 Ignition 就登场了,它会根据 AST生成字节码,并解释执行字节码。
其实一开始 V8 并没有字节码,而是直接将 AST 转换为机器码,由于执行机器码的效率是非常高效的,所以这种方式在发布后的一段时间内运行效果是非常好的。但是随着Chrome 在手机上的广泛普及,特别是运行在 512M 内存的手机上,内存占用问题也暴露出来了,因为 V8 需要消耗大量的内存来存放转换后的机器码。
为了解决内存占用问题,V8 团队大幅重构了引擎架构,引入字节码,并且抛弃了之前的编译器,最终花了将进四年的时间,实现了现在的这套架构。那什么是字节码呢?为什么引入字节码就能解决内存占用问题呢?字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

前端V8引擎(一)编译器和解释器

从图中可以看出,机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。

四、执行代码

生成字节码之后,接下来就要进入执行阶段了。
通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
V8 的解释器和编译器的取名也很有意思。解释器 Ignition 是点火器的意思,编译器 TurboFan 是涡轮增压的意思,寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率,因为热点代码都被编译器 TurboFan 转换了机器码,直接执行机器码就省去了字节码“翻译”为机器码的过程。
其实字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)。具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
对于 JavaScript 工作引擎,除了 V8 使用了“字节码 +JIT”技术之外,苹果的 SquirrelFish Extreme 和 Mozilla 的 SpiderMonkey 也都使用了该技术。
这么多语言的工作引擎都使用了“字节码 +JIT”技术,因此理解 JIT 这套工作机制还是很有必要的。你可以结合下图看看 JIT 的工作过程:

前端V8引擎(一)编译器和解释器

五、JavaScript 的性能优化

以上就是 V8 执行 JavaScript 代码的大致过程。在过去几年中,JavaScript 的性能得到了大幅提升,这得益于 V8 团队对解释器和编译器的不断改进和优化,比如今年谷歌IO大会上 V8引擎 再次得到提升。
虽然在 V8 诞生之初,也出现过一系列针对 V8 而专门优化 JavaScript 性能的方案,比如隐藏类、内联缓存等概念都是那时候提出来的。不过随着 V8 的架构调整,你越来越不需要这些微优化策略了,相反,对于优化 JavaScript 执行效率,你应该将优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上,主要关注以下三点内容:
  • 提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,加快页面响应交互;

  • 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;

  • 减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。

总结

V8 引擎自诞生之日起就以性能优化作为目标,引入了众多新技术,极大了带动了整个业界 JavaScript 引擎性能的快速发展。总的来说,V8引擎较为激进,青睐可以提高性能的新技术,而 JavaScriptCore 引擎较为稳健,渐进式的改变着自己的性能。
V8在不同的机器上使用与机器位数相匹配的数据表示,而在 JavaScriptCore 中句柄都是使用64位表示,其可以表示更大范围的数字,所以即使在32位机器上,浮点类型同样可以保存在句柄中,不再需要访问堆中的数据,当也会占用更多的空间。
所以得出,编译的基本单位全局代码、或者函数,比如下载完一个js文件,先编译这个js文件,但是js文件内定义的函数是不会编译的,等调用到该函数的时候Javascript 引擎才会去编译该函数。
总的来说,V8引入了 JIT 在运行时把 js 代码进行转换为机器码,V8 执行时间越长,执行效率越高,是因为更多的代码成为热点代码之后,转为了机器码来执行。此外,除了熟悉的V8引擎之外,常见的 JS 引擎还有: SpiderMonkey、Rhin、KJS 、Chakra、Nashorn 、JerryScript 等等。

特别注明:严格的讲,语言本身并不存在编译型或者是解释型,因为语言只是一些抽象的定义与约束,并不要求具体的实现,执行方式。这里讲 JS 是一门“解释型语言” 只是 JS 一般是被 JS 引擎动态解释执行,而并不是语言本身的属性。



以上是关于前端V8引擎编译器和解释器的主要内容,如果未能解决你的问题,请参考以下文章

How Javascript works (Javascript工作原理) 引擎,运行时,如何在 V8 引擎中书写最优代码的 5 条小技巧

浅析V8引擎,让你更懂JavaScript!

JavaScript工作机制:V8 引擎内部机制及如何编写优化代码的5个诀窍

V8引擎——详解

新 V8 即将推出和 Node.js

浏览器是如何工作的:Chrome V8让你更懂JavaScript