图解 Google V8 # 13:字节码:V8为什么又重新引入字节码?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解 Google V8 # 13:字节码:V8为什么又重新引入字节码?相关的知识,希望对你有一定的参考价值。

说明

图解 Google V8 学习笔记

什么是字节码?

字节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是一种中间码,它比机器码更抽象。它经常被看作是包含一个执行程序的二进制文件,更像一个对象模型。字节码被这样叫是因为通常每个 opcode 是一字节长,但是指令码的长度是变化的。每个指令有从 0 到 255(或十六进制的: 00 到FF)的一字节操作码,被参数例如寄存器或内存地址跟随。

所谓字节码,是指编译过程中的中间代码。

字节码有两个作用:

  1. 解释器可以直接解释执行字节码 ;
  2. 优化编译器可以将字节码编译为二进制代码,然后再执行二进制机器代码。

早期 V8 执行流水线:直接将 javascript 代码编译成机器代码。

  • 基线编译器,它负责将 JavaScript 代码编译为没有优化过的机器代码。
  • 优化编译器,它负责将一些热点代码(执行频繁的代码)优化为执行效率更高的机器代码。

  1. 首先,V8 会将一段 JavaScript 代码转换为抽象语法树 (AST)。
  2. 接下来基线编译器会将抽象语法树编译为未优化过的机器代码,然后 V8 直接执行这些未优化过的机器代码。
  3. 在执行未优化的二进制代码过程中,如果 V8 检测到某段代码重复执行的概率过高,那么 V8 会将该段代码标记为 HOT,标记为 HOT 的代码会被优化编译器优化成执行效率高的二进制代码,然后就执行该段优化过的二进制代码。
  4. 不过如果优化过的二进制代码并不能满足当前代码的执行,这也就意味着优化失败,V8 则会执行反优化操作。

为什么现在使用了字节码 + 解释器 + 编译器方式,抛弃了直接将 JavaScript 代码编译为二进制代码的方式,尽管机器代码的执行性能非常高效,出于什么原因考虑?

  • 时间问题:编译时间过久,影响代码启动速度;
  • 空间问题:缓存编译后的二进制代码占用更多的内存。

机器代码缓存

通过把二进制代码保存在内存中来消除冗余的编译,重用它们完成后续的调用,这样就省去了再次编译的时间。实践表明,在浏览器中采用了二进制代码缓存的方式,初始加载时分析和编译的时间缩短了 20%~40%。

V8 使用了两种代码缓存策略:

  1. 内存缓存(in-memory cache):V8 第一次执行一段代码时,会编译源 JavaScript 代码,并将编译后的二进制代码缓存在内存中。
  2. 硬盘缓存:即便关闭了浏览器,下次重新打开浏览器再次执行相同代码时,也可以直接重复使用编译好的二进制代码。


在早期,Chrome 用了这两种代码缓存的策略来提升 JavaScript 代码的执行速度,以牺牲存储空间来换取执行速度。

JavaScript 代码和二进制代码:


二进制代码所占用的内存空间是 JavaScript 代码的几千倍,V8 过度占用内存,会导致 Web 应用的速度大大降低。

为了解决缓存的二进制机器代码占用过多内存的问题,早期的 Chrome 并没有缓存函数内部的二进制代码,只是缓存了顶层次的二进制代码,采用了惰性编译,其实惰性编译除了能提升 JavaScript 启动速度,还可以解决部分内存占用的问题。

缺陷:如果浏览器只缓存顶层代码,那么闭包模块中的代码将无法被缓存,而对于高度工程化的模块来说,这种模块式的处理方式到处都是,这就导致了一些关键代码没有办法被缓存。

因此,V8 团队对早期的 V8 架构进行了非常大的重构,具体地讲,抛弃之前的基线编译器和优化编译器,引入了字节码、解释器和新的优化编译器。

字节码降低了内存占用

为什么通过引入字节码就能降低 V8 在执行时的内存占用呢?

字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码占用的空间远小于二进制代码,所以浏览器就可以实现缓存所有的字节码,而不是仅仅缓存顶层的字节码。

虽然采用字节码在执行速度上稍慢于机器代码,但是整体上权衡利弊,采用字节码也许是最优解。因为采用字节码除了降低内存之外,还提升了代码的启动速度,并降低了代码的复杂度,而牺牲的仅仅是一点执行效率。

字节码如何提升代码启动速度?

启动 JavaScript 代码的流程图:

  • 生成机器代码比生成字节码需要花费更久的时间
  • 直接执行机器代码却比解释执行字节码要更高效

V8 在使用的模型:

  • V8 的解释器叫 Ignition,(就原始字节码执行速度而言)是所有引擎中最快的解释器。
  • V8 的优化编译器名为 TurboFan,最终由它生成高度优化的机器码。

字节码如何降低代码的复杂度?

早期的 V8 代码,无论是基线编译器还是优化编译器,它们都是基于 AST 抽象语法树来将代码转换为机器码的,不同架构的处理器非常之多

这意味着基线编译器和优化编译器要针对不同的体系的 CPU 编写不同的代码,这会大大增加代码量。

引入了字节码,就可以统一将字节码转换为不同平台的二进制代码:

因为字节码的执行过程和 CPU 执行二进制代码的过程类似,相似的执行流程,那么将字节码转换为不同架构的二进制代码的工作量也会大大降低,这就降低了转换底层代码的工作量。字节码是平台无关的,机器码针对不同的平台都是不一样的。

总的来说字节码的优势有如下三点:

  • 解决启动问题:生成字节码的时间很短;
  • 解决空间问题:字节码占用内存不多,缓存字节码会大大降低内存的使用;
  • 代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。

参考资料

以上是关于图解 Google V8 # 13:字节码:V8为什么又重新引入字节码?的主要内容,如果未能解决你的问题,请参考以下文章

图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?

字节码

图解Google V8,搞懂 JavaScript 执行逻辑

图解 Google V8 # 01:V8 是如何执行一段 JavaScript 代码的?

图解 Google V8 # 06:原型链:V8是如何实现对象继承的?

图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?