字节码(一):V8为什么又重新引入字节码?
所谓字节码,是指编译过程中的中间代码。处于高级代码和机器代码之间,在V8中,字节码有两个作用:
第一个是解释器可以直接解释执行字节码;
第二个是优化编译器可以将字节码编译为二进制代码,然后再执行二进制机器代码。
观察上图可以看到,早期的V8也使用了两个编译器:
第一个是基线编译器,它负责将JavaScript代码编译为没有优化过的机器代码。
第二个是优化编译器,它负责将一些热点代码(执行频繁的代码)优化为执行效率更高的
机器代码。
了解这两个编译器之后,接下来我们再来看看早期的V8是怎么执行一段JavaScript代码的。
首先,V8会将一段JavaScript代码转换为抽象语法树(AST)。
接下来基线编译器会将抽象语法树编译为未优化过的机器代码。V8直接执行未优化的代码。
在执行未优化的二进制代码过程中,如果V8检测到某段代码重复执行的概率过高,那么V8会将该段代码标记为HOT,会将标记的代码转化为执行效率更高的二进制代码,然后执行的是优化之后的代码。
不过如果优化过的二进制代码并不能满足当前代码的执行,这也就意味着优化失败,V8则会执行反优化操作。
机器代码缓存
当JavaScript代码在浏览器中被执行的时候,需要先被V8编译,早期的V8会将JavaScript编译成未经优化的二进制机器代码,然后再执行这些未优化的二进制代码,通常情况下,编译占用了很大一部分时间,下面是一段代码的编译和执行时间图:
由上图可以看出,重复的代码每次都会被编译之后执行。
V8 使用两种代码缓存策略来缓存生成的代码。
V8在第一次执行代码的时候,编译代码之后,会将编译之后的二进制代码缓存在内存中。再次执行代码会先查找是否编译过这段代码。
除了将代码缓存在内存中策略之外,还会将代码缓存到硬盘上,这样即使关闭了浏览器,下次重新打开浏览器再次执行相同的代码,也可以直接重复使用编译好的二进制代码。
字节码降低了内存占用
在早期,Chrome做了两件事来提升JavaScript代码的执行速度:
第一,将运行时将二进制机器代码缓存在内存中;
第二,当浏览器退出时,缓存编译之后二进制代码到磁盘上。
JavaScript转换为二进制代码很占用内存。
V8采用了惰性编译,能够提升启动速度,有效解决部分内存占用问题,但存在很大不确定性。
根据惰性编译的原则,当V8首次执行上面这段代码的过程中,开始只是编译最外层的代码,那些函数内部的代码,如下图中的黄色的部分,会推迟到第一次调用时再编译。早期的Chrome并没有缓存函数内部的二进制代码,只是缓存了顶层次的二进制代码,比如上图中红色的区域。
比如我们多人开发的项目,通常喜欢将自己的代码封装成模块,在JavaScript中,由于没有块级作用域(ES6之前),所以我们习惯使用立即调用函数表达式(IIFEs),比如下面这样的代码:
test_module.js
var test_module = (function () {
var count_
function init_(){count_ = 0}
function add_(){count_ = count_+1}
function show_(){console.log(count_)}
return {
init: init_,
add: add_,
show:show_
}
})()
app.js
test_module.init()
test_module.add()
test_module.show()
test_module.add()
test_module.show()
上面就是典型的闭包代码,它将和模块相关的所有信息都封装在一个匿名立即执行函数表达式中,并将需要暴漏的接口数据返回给变量test_module。如果浏览器只缓存顶层代码,那么闭包模块中的代码将无法被缓存,而对于高度工程化的模块来说,这种模块式的处理方式到处都是,这就导致了一些关键代码没有办法被缓存。
字节码如何提升代码启动速度?
我们先看引入字节码是怎么提升代码启动速度的。下面是启动JavaScript代码的流程图:
从图中可以看出,生成机器代码比生成字节码需要花费更久的时间,但是直接执行机器代码却比解释执行字节码要更高效,所以在快速启动JavaScript代码与花费更多时间获得最优运行性能的代码之间,我们需要找到一个平衡点。
解释器可以快速生成字节码,但字节码通常效率不高。 相比之下,优化编译虽然需要更长时间处理,但最终会产生更高效的机器码,这正是 V8 在使用的模型。它的解释器叫Ignition,(就原始字节码执行速度而言)是所有引擎中最快的解释器。V8 的优化编译器名为 TurboFan,最终由它生成高度优化的机器码。
字节码如何降低代码的复杂度?
不同的架构的机器码是不一样的,直接转换为二机制代码,意味着基线编译器和优化编译器要针对不同的体系的CPU编写不同的代码,这会大大增加代码量。
引入了字节码,就可以统一将字节码转换为不同平台的二进制代码,你可以对比下执行流程:
因为字节码的执行过程和CPU执行二进制代码的过程类似,相似的执行流程,那么将字节码转换为不同架构的二进制代码的工作量也会大大降低,这就降低了转换底层代码的工作量。
此文章为5月Day19学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net