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

简介: 图解 Google V8 # 13:字节码(一):V8为什么又重新引入字节码?

说明

图解 Google V8 学习笔记



什么是字节码?


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


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


字节码有两个作用:


   解释器可以直接解释执行字节码 ;


   优化编译器可以将字节码编译为二进制代码,然后再执行二进制机器代码。


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


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


48ee9a3bfe6f4a16a788d8d31c4343ba.png


   首先,V8 会将一段 JavaScript 代码转换为抽象语法树 (AST)。


   接下来基线编译器会将抽象语法树编译为未优化过的机器代码,然后 V8 直接执行这些未优化过的机器代码。


   在执行未优化的二进制代码过程中,如果 V8 检测到某段代码重复执行的概率过高,那么 V8 会将该段代码标记为 HOT,标记为 HOT 的代码会被优化编译器优化成执行效率高的二进制代码,然后就执行该段优化过的二进制代码。


   不过如果优化过的二进制代码并不能满足当前代码的执行,这也就意味着优化失败,V8 则会执行反优化操作。


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


   时间问题:编译时间过久,影响代码启动速度;


   空间问题:缓存编译后的二进制代码占用更多的内存。



机器代码缓存


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


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


   内存缓存(in-memory cache):V8 第一次执行一段代码时,会编译源 JavaScript 代码,并将编译后的二进制代码缓存在内存中。


   硬盘缓存:即便关闭了浏览器,下次重新打开浏览器再次执行相同代码时,也可以直接重复使用编译好的二进制代码。


649567b82bd042e69235159774881a84.png

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

JavaScript 代码和二进制代码:


8c7dfb988f544e58a16b1e321f79c8e6.png


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


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

03e5dac25925487b9b92dc567ff3cf8c.png


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


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



字节码降低了内存占用


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


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


1b03b789eea14608b8efef9069041346.png


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



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


启动 JavaScript 代码的流程图:


fbe7b3988eb54365ac3948c97b746fce.png


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

V8 在使用的模型:

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




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


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


b53912efba4741e195d343ef1f0bd010.png


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

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


4d3ae297ccee41dfb878ea21786dea56.png




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


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


   解决启动问题:生成字节码的时间很短;


   解决空间问题:字节码占用内存不多,缓存字节码会大大降低内存的使用;


   代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。






目录
相关文章
|
缓存 JavaScript 前端开发
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
382 0
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
|
Web App开发 JavaScript 前端开发
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
154 0
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
|
算法 JavaScript Java
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
134 0
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
|
前端开发 JavaScript
图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?
图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?
169 0
图解 Google V8 # 19 :异步编程(二):V8 是如何实现 async/await 的?
|
消息中间件 前端开发 JavaScript
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
458 0
图解 Google V8 # 18 :异步编程(一):V8是如何实现微任务的?
|
消息中间件 程序员 Android开发
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
135 0
图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?
|
存储 缓存 索引
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
214 0
图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?
|
JavaScript 前端开发 编译器
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
184 0
图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?
|
JavaScript 前端开发 Java
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
360 0
图解 Google V8 # 14:字节码(二):解释器是如何解释执行字节码的?
|
自然语言处理 JavaScript 前端开发
图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?
图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?
175 0
图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?

热门文章

最新文章