美文网首页
V8引擎如何运行JS

V8引擎如何运行JS

作者: small_zeo | 来源:发表于2021-02-28 18:07 被阅读0次

    前言

    V8引擎如何编译和优化JS的

    image.png

    什么是V8

    官网: https://v8.dev/
    V8是一个接收JavaScript代码,编译代码然后执行的C++程序,编译后的代码可以在多种操作系统多种处理器上运行。
    Chrome浏览器JS的引擎是V8
    Nodejs的运行环境是V8引擎
    electron的底层引擎也是V8

    image.png

    V8主要负责的工作

    编译和执行JS代码、处理调用栈
    内存的分配、垃圾的回收

    早期的V8引擎是如何编译JS代码

    V8引擎曾在2017年做了一次大的架构调整
    追本溯源,了解一下早期的V8引擎是如何编译JS代码的
    一般来说,大部分JS引擎在编译和执行代码都会用到三个重要的文件
    重要组件: 解析器、解释器和编译器

    1. 解析器负责将源代码解析成抽象语法树AST

    解释器负责将AST解释成字节码bytecode,同时解释器也有直接解释执行bytecode的能力

    2. 编译器负责编译出运行更加高效的机器代码

    在V8早期5.9版本之前,V8引擎没有解释器,却有两个编译器,编译流程如下:

    JS由解析器解析后,生成AST抽象语法树,然后由Full-codegen编译器直接使用AST来编译出机器代码而不进行任何中间转换,Full-codefen编译器也被称为基准编译器,因为它生成的是一个基准的未被优化的机器代码,这样做的好处是当第一次执行JS的时候,就是直接使用了高效的机器代码,因为没有中间的字节码产生,所以就不需要解释器,当代码运行一段时间后,V8引擎中的分析线程,收集了足够的数据来帮助另一个编译器Crankshaft来做代码优化,然后需要优化的源码重新解析生成AST,然后Crankshaft使用生成好的AST再生成优化后的机器代码来提升运行的效率,所以Crankshaft的编译器又被称为优化编译器。这样的设计初衷是好的,减少抽象语法树到字节码的转换时间,提高外部浏览器中JS的执行的性能,但是这样的架构设计也带来了不少的问题:
    1. 生成的机器码会占用大量的内存

    这对于大内存的电脑还好说,但对于早期的安卓低内存的设备,基本是不能承受的,并且有些代码仅仅执行一次,没有必要直接生成机器码

    2. 缺少中间层机器码,无法实现一些优化策略,导致V8引擎性能提升缓慢
    3. 之前的编译器无法很好的支持和优化JS的新语法特性
    因此,V8团队为了解决这些问题,用了三年半时间开发了一套新的V8架构,V8团队称之为设计顶峰的新架构:
    image.png

    新的V8架构是怎样的?

    语法树的解析基本还是保持一致的;但在获得抽象语法树之后,V8引擎加入了解释器lgnition,语法树通过解释器lgnition生成了bytecode字节码,此时AST就被清除掉了,释放内存空间,生成的bytecode直接被解释器执行,同时生成的bytecode将作为基准执行模型,字节码更加简洁,生成的bytecode大小相当于等效的基准机器代码的25%到50%,在代码不断的运行过程中,解释器收集到了很多可以用来优化代码的信息,比如变量的类型,哪些函数执行的频率较高,这些信息被发送给编译器,V8引擎新的编译器TurboFan会根据这些信息和字节码,来编译出经过优化的机器代码。
    image.png

    V8引擎在处理JS过程中的一些优化策略:

    1.函数只声明未被调用,不会被解析生成AST,也就不会生成字节码
    2.函数只被调用一次,bytecode直接被解释执行,TureboFan不会进行优化编译
    3.函数被调用多次,可能会被标记为热点函数,可能会被编译成机器代码。当lgnition解释器收集的类型信息确定后,这是TurboFan则会将bytecode编译为优化后的机器代码,以提高代码的执行性能,最后执行这个函数时,就直接运行优化后的机器代码。


    image.png
    所以整体来说,就是处于一个运行字节码和优化的机器代码共存的一个状态,随着JS源码不断的被执行,会有更多的源码被标记为热点代码,就会产生更多的机器代码。

    逆向还原(deoptimization)

    在某些情况下,优化后的机器码可能会被逆向还原称字节码,这个过程叫做deoptimization,这是因为JavaScript是一个动态语言,会导致一个lgnition收集到的信息是错误的

    例如:有一个sum函数,在函数声明时,JS引擎并不知道参数x,y是什么类型,但在后面多次调用中,传入的x,y都是整型,sum函数被识别为热点函数,解释器将收集到的类型信息和该函数对应的字节码发送给编译器,于是编译器生成的优化后的机器代码中就假定了sum函数的参数x,y都是整型,之后遇到该函数的调用就直接使用运行更快的机器代码,如果此时调用sum函数传入了字符串,机器代码不知道如何处理字符串的参数,于是就需要进行deoptimization回退字节码,由解释器来解释执行。
    因此我们尽量不要把一个变量的类型变来变去,对传入函数的参数的类型也是最好保持固定,否则会给V8引擎损失一定的性能
    function sum(x, y) {
      return x + y
    }
    
    image.png

    新V8架构的好处

    不需要一开始直接编译成机器码,而是生成了中间层的字节码,字节码的生成速度远远大于机器码的,所以网页初始化解析执行JS的时间缩短了,网页就可以更快的onload
    在生成的优化机器代码时,不需要从源码重新编译,而使用字节码,并且当需要deoptimization时,只需要回归到中间层的字节码解释执行就可以了
    新的架构在性能上带来了很大的提升


    image.png

    参考学习视频: https://www.bilibili.com/video/BV1zV411z7RX

    相关文章

      网友评论

          本文标题:V8引擎如何运行JS

          本文链接:https://www.haomeiwen.com/subject/oemgfltx.html