V8 fast access

536 阅读5分钟

Property & element

Js 对象的字段在v8里一共有两种类型: Named Property 和 Integer Indice。

那咋区分这两种字段呢? 简单的理解, 比如有对象const obj = {a: 'foo', b: 'bar', 1: "zoo"}, 其中,字段’a’、’b’就是 Named Properties, 简称property, 1 就是 integer indice, 简称element.

v8在存储这两种字段的时候也做了区分, property字段的值被存在properties store, element字段的值被存到了 elements store。 而element store 和 properties store 的数据类型都有可能是 数组或者是 字典。

hidden class

因为js是门动态语言,没办法像静态语言(比如java)在编译的时候知道每个对象到底会有哪些字段,各个字段的类型,所以也就不知道要分配多少空间,每个字段到底存在哪个内存地址。

那咋办呢?

v8的做法是给每个对象,额外生成一个叫hidden class的对象,来存储这个对象的信息。

每一个js对象第一位就存着一个指向自己hidden class的指针。对于property来说,hidden class里最重要的就是图中的 bit field 3, 这里存储了对象里有几个字段,以及一个指向descriptor array的指针。 而descriptor array则存储了字段名、字段值地址等关键信息。

关于hidden class 最重要的一条规则就是, 当对象的字段名一样,且各个字段生成的顺序也一样的时候,这些对象应该共享同一个hidden class。

那么hidden class 具体是怎么生成的呢?

  • 在执行到 var o = {} 的时候, v8会给对象o生成一个空的hidden class c0。
  • 执行o.a = 'foo', v8会再生成一个新的hidden class c1用于存储字段a的信息,同时c1还会包含一个指向c0的指针。c1生成好之后,对象o第一位的hidden class 指针会直接指向c1.
  • 以此类推,在执行o.b = 'bar'; o.c ='baz'之后,v8还会再生成两个hidden class c2,c3,最终o的hidden class将会指向c3. 这整个hidden class的链路叫做 transition chain

这里有一个非常重要的点,即生成字段顺序必须一致,如果把 o.b = "bar"o.c = "baz" 给换一下位置,先给字段c赋值,最终o会指向和上面c3(包含字段c信息)完全不同的hidden class c3`(包含字段b信息)

Named Property 类型

in-object property

直接存在对象上,访问速度要多快有多快。 那哪些字段能存到对象上呢? 主要取决于对象分配的空间以及对象字段的多少, 基本可遇不可求。

fast property

当js对象的property store(第二位properties指针指向的地址)为一个数组时,我们讲这是个fast property对象。 每个fast property在property store里的index值,就需要从上面讲到的hidden class里面去读取。

slow property

当js对象的property store为一个字典的时候,我们讲这是个slow property对象。 这个时候所有的字段值都存在字典里,每次查询都需要经过类似hash函数计算这种操作,效率相当的低。

啥时候会出现这种情况呢? Hidden class 的transition chain太长了(查字段爬起来太累),或者手欠写了一些骚操作代码,比如delete xxx字段(会生成一个新的hidden class,作用是告诉引擎别用hidden class了)

Elements 类型

Packed Elements

简单来说就是index连续的对象,比如 const arr = [0,1,2,3,4,5] 或者是const obj = {0: 'a', 1:'b', 2: 'c'}

holey elements

对应的,如果是连续的index中间断掉了,比如const arr = [0,,,1,2] 或者是const obj = {0: 'a', 1:'b', 999: 'c'}, 这种就叫holey elements.

其实其他还有很多种分类,细节可以看fast-properties

那这些类型对elemtns store 有啥影响呢? 如果一个对象的字段太过分散,比如const obj = {0: 'a', 999: 'c'}, 没必要为了存2个值就维护一个10k的数组,这种时候v8就会用字典来存储这个对象的字段,导致访问字段效率低下。 反之,如果字段都连续,那就会用数组来存,效率大大提升

inline cache

光是用hidden class来查字段在property store中的坐标可以一定程度上提升查询性能,但是还遗留了一些问题,比如当transition chain比较深的时候,查询需要访问很多层的hidden class能也不是特别可观。另外如果前后两次一样的操作,第一次查一遍,第二次访问还得再查一遍,感觉也没啥必要。

为了解决这些问题,v8采用了一种叫inline cache的技术。

举个例子:

function makePoint(x,y) {
    const point = {}
    point.x = x;
    point.y = y;
    return point;
}

上面这段代码,在被编译后,会被转译为类似以下的代码:

function makePoint(x,y) {
    const point = new Table()  // 这里Table想成对象的底层实现,每个table实例都有一个对应的hidden class
    Store(point, 'x', x) // 把值x存到point的x字段上
    Store(point, 'y', y)
    return point;
}

而Store呢会统一检查point的hidden class之前有没有出现过,如果有,就把上次的结果直接返回,省去爬transition chain的耗时(其实细节比这复杂,有兴趣可以看看这里)。

而每个Store会存多少种hidden class呢?v8有如下定义:

  1. 当只传入1种hidden class,则直接返回缓存中的值,这种状态叫monomorphism
  2. 当传入4种以下hidden class,需要先做条件判断,再决定返回的值,这种状态叫做polymorphism
  3. 当传入大于4种hidden class,这个时候,v8有维护一个全局的哈希表,可以直接拿着hidden class去哈希表查值。因为哈希表大小有限,遇到冲突的时候直接覆盖,所以会出现查不到的情况。这种情况就需要再跑回去重新爬transition chain了。

别小看这三种状态的差别,数据量大一点的时候,monomorphism状态的访问会比megamorphism快差不多100倍。 benchmark

参考文献

  1. fast-properties
  2. Explaining JavaScript VMs in JavaScript - Inline Caches
  3. What's up with monomorphism?
  4. A tour of V8: object representation