1、V8的演进历史
2008年V8发布第一个版本,当时的V8架构比较激进,直接将js代码编译为机器码并执行,所以执行速度很快,但是只有Codegen一个编译器,所以对代码的优化很有限。
2010年V8发布了Crankshaft编译器,js代码会先被Full-Codegen编译器编译,如果后续改代码块会被多次执行,则会用Crankshaft编译器重新编译,生成更优化的代码,之后就使用优化后的代码来执行,进而提升性能。
Crankshaft编译器对代码的优化有限,所以2015年V8中加入了TurboFan编译器,此时V8依旧是直接将源码编译为机器码执行,这种架构存在一个核心问题,内存消耗特别大(通常一个几KB的文件,转换为机器码可能就是几十MB,这会小号巨大的内存空间)。
2016年V8加入了Ignition编译器,重新引入字节码,旨在减少内存使用。
2017年V8正式发布全新编译pipeline,它使用Ignition和TurboFan的组合来编译执行代码,从这(V8的5.9版本)开始,早期的Full-Codegen和Crankshaft编译器不再用来执行js,在最新的架构中,最核心的模块有三个:解析器(Parser)、解释器(Ignition)、优化编译器(TurboFan)。
当V8执行js源码时,首先,解析器会把源码解析为抽象语法树(Abstract Syntax Tree),解释器再将AST翻译为字节码,一边解释一边执行,在此过程中,解释器会记录特定代码片段的运行次数,如果运行次数超过了某个阈值,该段代码就被标记为热代码(hot code),并将运行信息反馈给优化编译器(TureboFan),优化编译器根据反馈信息,优化并编译字节码,最终生成优化后的机器码,这样,当该段代码再次被执行时,解释器就直接使用优化后的机器码执行,不用再次解释,从而大大提高了代码运行效率,这种在运行时编译代码的技术叫即时编译(JIT)。
2、V8的解析器
将js源码解析为AST,此过程会经过词法分析、语法分析,通过预解析提高执行效率。
词法分析:将js源码解析为一个个最小单元的token。
在V8中,Scanner负责接收Unicode字符流,并将其解析为tokens提供给解析器使用。
语法分析:根据语法规则,将tokens组成一个具有前台层级的抽象语法树,在这个过程中,如果源码不符合语法规范,解析过程就会终止,并抛出语法错误。
对于一份js源码,如果所有源码都要经过解析才能执行,那必然会面临三个问题:1、一次性解析所有代码,代码执行时间变长,2、内存消耗增加,因为解析完的AST以及根据AST编译后的字节码都会存放在内存中,3、占用磁盘空间,编译后的代码会缓存在磁盘上。
因此,现在主流的浏览器都会进行延迟解析,在解析过程中,对于不是立即执行的函数,只进行预解析(Pre Parser),只有当函数调用时才对函数进行全量解析。进行预解析时,只验证函数的语法是否有效,解析函数声明,确定函数作用域,不生成AST。实现预解析的就是Pre-Parser解析器。
3、V8的解释器
Js源码转换为CPU可识别的机器码,需要消耗巨大的内存,V8为了解决内存内存占用问题引入了字节码。字节码是对机器码的抽象,语法与汇编有些类似,可以把它看做一个一个的指令。
解析器Ignition根据AST生成字节码并执行。
这个过程中会收集反馈信息,交给TurboFan进行优化编译。TurboFan根据Ignition收集的反馈信息,将字节码编译为优化后的机器码,后续Ignition有优化后的机器码代替字节码执行。
4、V8的优化编译器
Ignition解释器在执行字节码时,依旧需要将字节码转换为机器码,因为CPU只能识别机器码,虽然多了一层字节码的转换,看起来效率低了,但是相比于机器码,基于字节码可以更方便的进行性能优化,其中最主要的优化就是使用TurboFan编译器编译热点代码。Ignitio解释器在解释执行的过程中,会标记重复执行的热点代码,这些被标记的代码,会被TurboFan编译器编译生成效率更高的机器码。
TurboFan在工作的时候主要用到了两个算法,一个内联,一个是逃逸分析。
内联就是对嵌套函数进行内联分析,如下图左侧代码,如果不经优化,直接编译该段代码,则会生成两个函数的机器码,但为了进一步提升性能,TurboFan就会对这两个函数进行内联,然后在编译,如下提中间代码,更进一步,由于函数内部变量的值都是确定的,所以函数还可以进一步优化,如下图右侧代码。最终生成的机器码相比优化前少了非常多,执行效率自然也就高了。通过内联,可以降低复杂度,消除冗余代码,合并常量,并且,内联技术通常也是逃逸分析的基础。
逃逸分析是分析对象的生命周期是否仅限于当前函数,如果对象是在函数内部定义的,且对象只作用于函数内部,比如对象没有被返回,也没有传递或者给其他函数调用,此时,这个对象会被认为是”未逃逸”的。在编译优化时,会使用标量替换掉未逃逸的对象,以减少对象定义,从而减少从内存中访问对象属性,提升了执行效率的同时,还减少了内存的使用。
文章来源于视频:https://www.zhihu.com/zvideo/...