V8 原理之对象管理
对象¶
对象的结构¶
在 V8 中,对象主要由三个指针构成,分别是隐藏类(Hidden Class),Property 还有 Element。
- 对象中的 数字属性称为可索引属性(Elements),字符串属性被称为命名属性(Properties)。
- 可索引属性应该按照索引值大小升序排列,而命名属性根据创建的顺序升序排列。
命名属性三种存储方式¶
V8 中 命名属性 有三种的不同存储方式:对象内属性(in-object)、快属性(fast) 和 慢属性(slow)。
- 对象内属性 保存在对象本身,提供最快的访问速度。
- 快属性 比对象内属性多了一次寻址时间。
- 慢属性 与前面的两种属性相比,会 存储属性的完整结构(另外两种属性的结构在隐藏类中描述),速度最慢(慢属性、属性字典、哈希存储说的都是一回事)。
隐藏类¶
- 实现对象属性的快速存取,V8 中引入了 Map 数据结构,即 隐藏类(Hidden Class),来存放描述命名属性。
- 对象属性的 Attribute 被描述为以下结构:
[[Value]]
:属性的值[[Writable]]
:定义属性是否可写(即是否能被重新分配)[[Enumerable]]
:定义属性是否可枚举[[Configurable]]
:定义属性是否可配置(删除)
- 隐藏类的引入,将属性的 Value 与其它 Attribute 分开; 对象的 Value 是经常发生变动,而 Attribute 几乎不怎么变的。
隐藏类的创建¶
- 每添加一个命名属性,都会对应一个生成一个新的隐藏类。
- V8 的底层实现了一个将隐藏类连接起来的转换树,如果以相同的顺序添加相同的属性,转换树会保证最后得到相同的隐藏类。
delete 操作¶
- 从快照可以看到,删除了 a.a 后,a 变成了慢属性,退回哈希存储;
- 如果我们按照添加属性的顺序逆向删除属性,a 也没有退回哈希存储。
结论与启示¶
- 属性分为命名属性和可索引属性,命名属性存放在 Properties 中,可索引属性存放在 Elements 中。
- 命名属性有三种不同的存储方式:对象内属性、快属性和慢属性,前两者通过线性查找进行访问,慢属性通过哈希存储的方式进行访问。
- 总是以相同的顺序初始化对象成员,能充分利用相同的隐藏类,进而提高性能。
- 增加或删除可索引属性,不会引起隐藏类的变化,稀疏的可索引属性会退化为哈希存储。
- delete 操作可能会改变对象的结构,导致引擎将对象的存储方式降级为哈希表存储的方式,不利于 V8 的优化,应尽可能避免使用(当沿着属性添加的反方向删除属性时,对象不会退化为哈希存储)。
继承¶
- 继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 通过原型和原型链的方式来实现了继承特性。
__proto__
指向的对象称为该对象的原型对象 (prototype)。
构造函数创建对象¶
V8 在背后悄悄地做了以下几件事情:
var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, 'black');
内联缓存¶
- 内联缓存(Inline Cache)的原理:在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的 关键中间数据,然后缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。
- IC 会为每个函数维护一个 反馈向量 (FeedBack Vector),反馈向量记录了函数在执行过程中的一些关键的中间数据。
- 反馈向量其实就是一个表结构,它由很多项组成的,每一项称为一个插槽 (Slot),V8 会依次将执行函数 f 的中间数据写入到反馈向量的插槽中。
- 当 V8 再次调用函数 f 时,比如执行到函数 f 中的 return obj.x 语句时,它就会在对应的插槽中查找 x 属性的偏移量,之后 V8 就能直接去内存中获取 obj.x 的属性值了。这样就大大提升了 V8 的执行效率。
堆栈内存模型¶
栈空间¶
- 栈空间用来管理 JavaScript 函数调用。
- 在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等。
- 栈空间的最大的特点是 空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率高。
堆空间¶
- 堆空间是一种树形的存储结构,用来存储对象类型的离散的数据。
- JavaScript 中除了原生类型的数据,其他的都是对象类型,诸如函数、数组,在浏览器中还有 window 对象、document 对象等,这些都是存在堆空间的。
总结¶
13 个 JavaScript 性能提升技巧:
- 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类);
- 总是以相同的次序初始化对象成员;
- 尽量使用可以用 31 位有符号整数表示的数;
- 为数组使用从 0 开始的连续的主键;
- 别预分配大数组(比如大于 64K 个元素)到其最大尺寸,令其尺寸顺其自然发展就好;
- 别删除数组里的元素,尤其是数字数组;
- 别加载未初始化或已删除的元素;
- 对于固定大小的数组,使用 "array literals" 初始化(初始化小额定长数组时,用字面量进行初始化);
- 小数组(小于 64k)在使用之前先预分配正确的尺寸;
- 请勿在数字数组中存放非数字的值(对象);
- 尽量使用单一类型(monomorphic)而不是多类型(polymorphic)(如果通过非字面量进行初始化小数组时,切勿触发类型的重新转换);
- 不要使用 try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);
- 在优化后避免在方法中修改隐藏类。