美文网首页
JS引擎与字节码

JS引擎与字节码

作者: FingerStyle | 来源:发表于2022-01-09 22:02 被阅读0次

什么是字节码?

字节码(Byte-code)是一种包含执行程序,由一序列 op 代码/数据对组成的二进制文件,是一种中间码(IR)。是机器码的一种抽象。

我们常说的字节码一般是Java字节码,但其实很多动态编译解释的语言都有字节码,比如Javascript, python、ruby。

那么,字节码长什么样?我们用文本编辑器打开对应的文件,可以看到里面都是些二进制的字符

image-20220109210401562.png

下图是V8的JS源码、字节码和机器码

img

Hermes的JS源码:

print("helloword");

Hermes的字节码

Bytecode File Information:
  Bytecode version number: 83
  Source hash: 0000000000000000000000000000000000000000
  Function count: 1
  String count: 3
  String Kind Entry count: 2
  RegExp count: 0
  Segment ID: 0
  CommonJS module count: 0
  CommonJS module count (static): 0
  Bytecode options:
    staticBuiltins: 0
    cjsModulesStaticallyResolved: 0

Global String Table:
s0[ASCII, 0..5]: global
s1[ASCII, 6..14]: helloword
i2[ASCII, 15..19] #A689F65B: print

Function<global>(1 params, 11 registers, 0 symbols):
Offset in debug table: source 0x0000, lexical 0x0000
    GetGlobalObject   r0
    TryGetById        r2, r0, 1, "print"
    LoadConstUndefined r1
    LoadConstString   r0, "helloword"
    Call2             r0, r2, r1, r0
    Ret               r0

Debug filename table:
  0: /tmp/hermes-input.js

Debug file table:
  source table offset 0x0000: filename id 0

Debug source table:
  0x0000  function idx 0, starts at line 1 col 1
    bc 2: line 1 col 1
    bc 14: line 1 col 6
  0x000a  end of debug source table

Debug lexical table:
  0x0000  lexical parent: none, variable count: 0
  0x0002  end of debug lexical table

为什么需要字节码?

首先,我们知道JS是一种脚本语言,他运行在不同的平台,包括Windows、Linux、MacOS、iOS、Android 等。不管什么样的语言,最终都是要变成机器码的,而不同平台由于有不同的处理器架构以及对应的指令集,所以就需要有一个中间层来负责对应平台的指令转换成对应的机器码,使得脚本语言的开发者无需关心底层这些复杂的硬件和指令系统。这个中间层就是我们说的虚拟机,而在虚拟机上面执行的代码就是字节码。

虚拟机是一种的抽象化的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。虚拟机有自己完善的硬体架构,如处理器堆栈寄存器等,还具有相应的指令系统。虚拟机屏蔽了与具体操作系统平台相关的信息,使得程序只需生成在虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

例如JavascriptCore内部的SquirrelFish、V8的Ignition,以及React Native专用的Hermes。

以V8为例,现在带有字节码的JS引擎工作流程一般如下:

2017

但事实上,V8早期时没有使用字节码,而是直接从JS转换成机器码

2010

这样执行的性能确实比从JS转成字节码再转成机器码要更好,但后面他们发现这样做有一些弊端:

  1. 机器码占空间很大。在V8执行的过程会将js源代码转化成二进制代码并且将二进制代码存储到内存中,退出进程后会将二进制代码存储到硬盘上。将js源码转化成的二进制代码占用的内存空间是非常巨大的,如果说一个js源码的文件大小是1M,那么生成的二进制代码可能就是十几M,而早期手机的内存普遍不高,过度占用会导致性能大大降低。

  2. 代码复杂度太高。上文提到过不同的CPU架构对应的指令集是完全不同的,而市面上CPU架构的种类又非常多,那么将AST转化为二进制代码的Full-Codegen引擎以及优化编译的Crankshaft引擎要针对不同的CPU架构编写代码,这个复杂程度及工作量可想而知,而对字节码进行编译可以大大的减少这个工作量


    v8.png
  1. 重复编译bug

    Bug的报告人在当时的Chrome浏览器下重复加载Facebook,并打开了各项监控发现:第一次加载时 v8.CompileScript 花费了 165 ms,而重复加载时发现真正耗时高的js代码并没有被缓存,导致重复加载时编译的时间和第一次加载的消耗大致相同。

导致这个bug的原因其实也很好理解,之前提到过因为二进制代码占用内存空间大,根据惰性编译的优化原则,所以V8并不会将所有代码进行编译只会编译最外层的代码,而在函数内部的代码会在第一次调用时编译,比如:


v8 function.png

如果浏览器只缓存最外层代码,那么对我们前端高度工程化的模块来说会导致里面的关键代码却无法被缓存,这也是导致上述问题的主要原因。

而引入字节码之后,上面的三个问题就可以得到缓解。通过恰当地设计字节码的编码方式,字节码可以做到比机器码紧凑很多。

memory.png

启动时只需要编译出字节码,然后逐句执行字节码,编译出字节码的速度可远远快于编译出二进制代码的速度。


pageload.png

字节码的形式

先看一段JS代码

function incrementX(obj) {
  return 1 + obj.x;
}

incrementX({x: 42});

V8字节码

编译为V8字节码是这样的

$ node --print-bytecode incrementX.js
...
[generating bytecode for function: incrementX]
Parameter count 2
Frame size 8
  12 E> 0x2ddf8802cf6e @    StackCheck
  19 S> 0x2ddf8802cf6f @    LdaSmi [1]
        0x2ddf8802cf71 @    Star r0
  34 E> 0x2ddf8802cf73 @    LdaNamedProperty a0, [0], [4]
  28 E> 0x2ddf8802cf77 @    Add r0, [6]
  36 S> 0x2ddf8802cf7a @    Return
Constant pool (size = 1)
0x2ddf8802cf21: [FixedArray] in OldSpace
 - map = 0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)>
 - length: 1
           0: 0x2ddf8db91611 <String[1]: x>
Handler Table (size = 16)

我们稍微解释下其中一些指令的意思

LdaSmi [1]

LdaSmi [1] 将常量 1 加载到累加器中。

img

Star r0

接下来,Star r0 将当前在累加器中的值 1 存储在寄存器 r0 中。

img

LdaNamedProperty a0, [0], [4]

LdaNamedProperty 将 a0 的命名属性加载到累加器中。ai 指向 incrementX() 的第 i 个参数。在这个例子中,我们在 a0 上查找一个命名属性,这是 incrementX() 的第一个参数。该属性名由常量 0 确定。LdaNamedProperty 使用 0 在单独的表中查找名称:

- length: 1
           0: 0x2ddf8db91611 <String[1]: x>

可以看到,0 映射到了 x。因此这行字节码的意思是加载 obj.x。

那么值为 4 的操作数是干什么的呢? 它是函数 incrementX() 的反馈向量的索引。反馈向量包含用于性能优化的 runtime 信息。

现在寄存器看起来是这样的:

img

Add r0, [6]

最后一条指令将 r0 加到累加器,结果是 43。 6 是反馈向量的另一个索引。

img

Return

Return 返回累加器中的值。返回语句是函数 incrementX() 的结束。此时 incrementX() 的调用者可以在累加器中获得值 43,并可以进一步处理此值。

乍一看,V8 的字节码看起来非常奇怪,特别是当我们打印出所有的额外信息。但是一旦你知道 Ignition 是一个带有累加器寄存器的寄存器,你就可以分析出大多数字节码都干了什么。

Hermes字节码

同样一段代码,用Hermes生产的字节码如下(导出成了可阅读格式):

Bytecode File Information:
  Bytecode version number: 83
  Source hash: 0000000000000000000000000000000000000000
  Function count: 2
  String count: 3
  String Kind Entry count: 2
  RegExp count: 0
  Segment ID: 0
  CommonJS module count: 0
  CommonJS module count (static): 0
  Bytecode options:
    staticBuiltins: 0
    cjsModulesStaticallyResolved: 0

Global String Table:
s0[ASCII, 0..5]: global
i1[ASCII, 6..15] #FC148982: incrementX
i2[ASCII, 16..16] #0001E7F9: x

Function<global>(1 params, 11 registers, 0 symbols):
Offset in debug table: source 0x0000, lexical 0x0000
    DeclareGlobalVar  "incrementX"
    CreateEnvironment r0
    CreateClosure     r1, r0, 1
    GetGlobalObject   r0
    PutById           r0, r1, 1, "incrementX"
    GetByIdShort      r2, r0, 1, "incrementX"
    NewObject         r1
    LoadConstUInt8    r0, 42
    PutNewOwnByIdShort r1, r0, "x"
    LoadConstUndefined r0
    Call2             r0, r2, r0, r1
    Ret               r0

Function<incrementX>(2 params, 2 registers, 0 symbols):
Offset in debug table: source 0x0010, lexical 0x0000
    LoadParam         r0, 1
    GetByIdShort      r1, r0, 1, "x"
    LoadConstUInt8    r0, 1
    Add               r0, r0, r1
    Ret               r0

Debug filename table:
  0: /tmp/hermes-input.js

Debug file table:
  source table offset 0x0000: filename id 0

Debug source table:
  0x0000  function idx 0, starts at line 1 col 1
    bc 14: line 1 col 1
    bc 20: line 5 col 1
    bc 30: line 5 col 12
    bc 36: line 5 col 11
  0x0010  function idx 1, starts at line 1 col 1
    bc 3: line 2 col 17
    bc 11: line 2 col 10
  0x001a  end of debug source table

Debug lexical table:
  0x0000  lexical parent: none, variable count: 0
  0x0002  end of debug lexical table

Hermes字节码的可读版本可以通过https://hermesengine.dev/playground/ 或者 hermes test1.js -O -dump-bytecode显示出来(这里test1.js 的内容就是上面的JS代码)

同样的,这里节选incrementX 这个函数里面的指令说明下Hermes的字节码指令:

LoadParam r0, 1

读取第1个参数(也就是obj),保存到r0寄存器。

GetByIdShort r1, r0, 1, "x"

从r0(obj)中读取"x"属性,保存到r1寄存器。这里第3个参数1 是用来加速的缓存索引。

LoadConstUInt8 r0, 1

读取整数1, 保存到r0。 由于上一步已经使用过r0了,所以r0的值没有再保存的必要,这里重新给他赋值为整数1。

Add r0, r0, r1

把r0+r1的值写入r0,也就是obj.x + 1

Ret r0

返回r0的值

对比V8和Hermes的指令,我们可以看到不同的JS引擎生成的指令作用是类似的,只是指令的内容和顺序不太一样。相比而言,V8多了一个累加器,专门用于做数字运算,对于属性名的获取更偏向于使用索引而不是直接用字符串(不确定是不是导出成可阅读格式时填充上去的),操作数也是能少则少。这样做无疑字节码会更精简,但对于阅读来说就比较麻烦了。

参考链接

https://juejin.cn/post/6844904152745639949

https://zhuanlan.zhihu.com/p/28590489

https://hermesengine.dev/docs/design

相关文章

  • JS引擎与字节码

    什么是字节码? 字节码(Byte-code)是一种包含执行程序,由一序列 op 代码/数据对组成的二进制文件,是一...

  • 「转」V8基本概念

    架构图 现在 JS 引擎的执行过程大致是:源代码 --->抽象语法树 --->字节码 --->JIT--->本地代...

  • 静态代码扫描工具

    静态代码扫描工具/引擎对比: 序号引擎分析对象备注1Findbugs字节码缺陷模式匹配、数据流分析。通过字节码分析...

  • 05.局部变量表与操作数栈

    1) 概述 JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果。 ...

  • 字节码引用检测原理与实战

    一、字节码与引用检测 1.1 Java字节码 本章中的字节码重点研究Java 字节码,Java字节码(Java b...

  • 字节码执行引擎

    运行时栈帧结构 栈帧是虚拟机栈中的元素,每一个方法的调用对应着一个栈帧的入栈出栈。栈帧包括局部变量表、操作数栈、动...

  • 字节码执行引擎

    物理机的执行引擎是建立在处理器、硬件、指令集和操作系统层面上的。虚拟机的执行引擎是自己实现的,可以自行指定指令集和...

  • 字节码执行引擎

    一. 运行时栈结构 在介绍运行时栈结构之前,我们先回忆一下虚拟机运行时数据区: 本部分所讲述的运行时栈结构就是对程...

  • 虚拟机系列 | 执行引擎和垃圾回收

    一、执行引擎 应用程序经过编译,转换为字节码文件,字节码加载到内存空间并不能直接在操作系统上执行,执行引擎作为Ja...

  • Java虚拟机--虚拟机字节码执行引擎

    Java虚拟机--虚拟机字节码执行引擎 所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字...

网友评论

      本文标题:JS引擎与字节码

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