快属性和慢属性:V8是怎样提升对象属性访问速度的?

常规属性(properties)和排序属性(element)

参考以下代码:function Foo() {
    this[100] = 'test-100'
    this[1] = 'test-1'
    this["B"] = 'bar-B'
    this[50] = 'test-50'
    this[9] = 'test-9'
    this[8] = 'test-8'
    this[3] = 'test-3'
    this[5] = 'test-5'
    this["A"] = 'bar-A'
    this["C"] = 'bar-C'
}
var bar = new Foo()
for(key in bar){
    console.log(`index:${key} value:${bar[key]}`)
}

打印出来如下:

index:1 value:test-1
index:3 value:test-3
index:5 value:test-5
index:8 value:test-8
index:9 value:test-9
index:50 value:test-50
index:100 value:test-100
index:B value:bar-B
index:A value:bar-A
index:C value:bar-C

从结果可以总结出:
设置的数字属性被最先打印出来了,并且是按照数字大小的顺序打印的;设置的字符串属性依然是按照之前的设置顺序打印的,比如我们是按照B、A、C的顺序设置的,打印出来依然是这个顺序。
之所以出现这样的结果,是因为在ECMAScript规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列
在这里我们把对象中的数字属性称为排序属性,在V8中被称为elements,字符串属性就被称为常规属性,在V8中被称为properties。

elements属性和properties属性

elements属性指向了elements对象,在elements对象中,会按照顺序存放排序属性,properties属性则指向了properties对象,在properties对象中,会按照创建时的顺序保存了常规属性。

快属性和慢属性

将不同的属性分别保存到elements属性和properties属性中,无疑简化了程序的复杂度,但是在查找元素时,却多了一步操作。例如Bar.B,会读取两次,影响效率。
基于这个原因,V8采取了一个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,我们把这称为对象内属性(in-object properties)

采用对象内属性之后,常规属性就被保存到bar对象本身了,这样当再次使用bar.B来查找B的属性值时,V8就可以直接从bar对象本身去获取该值就可以了,这种方式减少查找属性值的步骤,增加了查找效率。
通常,我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。

因此,如果一个对象的属性过多时,V8就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构(词典)作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。

实践:在Chrome中查看对象布局

function Foo(property_num,element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`
this[ppt] = ppt
}
}
var bar = new Foo(10,10)

创建了函数对象,接下来我们就来看看构造函数和对象在内存中的状态。你可以将Chrome开发者工具切换到Memory标签,然后点击左侧的小圆圈就可以捕获当前的内存快照,最终截图如下所示:

上图就是收集了当前内存快照的界面,要想查找我们刚才创建的对象,你可以在搜索框里面输入构造函数Foo,Chrome会列出所有经过构造函数Foo创建的对象,如下图所示:


观察上图,我们搜索出来了所有经过构造函数Foo创建的对象,点开Foo的那个下拉列表,第一个就是刚才创建的bar对象,我们可以看到bar对象有一个elements属性,这里面就包含我们创造的所有的排序属性。内存布局如下:

  • 10个常规属性作为对象内属性,存放在bar函数内部;
  • 10个排序属性存放在elements中。
    var bar2 = new Foo(20,10)
    再生成内存快照

    内存布局如下:
  • 10属性直接存放在bar2的对象内;
  • 10个常规属性以线性数据结构的方式存放在properties属性里面;
  • 10个数字属性存放在elements属性里面。var bar3 = new Foo(100,10)。

    内存布局如下:
  • 10属性直接存放在bar3的对象内;
  • 90个常规属性以非线性字典的这种数据结构方式存放在properties属性里面;
  • 10个数字属性存放在elements属性里面。

    其他属性

  • Map:属于隐藏类
  • __proto__:__proto__属性就是原型,是用来实现JavaScript继承的
此文章为5月Day10学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪

豪猪
4 声望4 粉丝

undefined