Skip to content

V8 原理之对象管理

对象

对象的结构

在 V8 中,对象主要由三个指针构成,分别是隐藏类(Hidden Class),Property 还有 Element。

  • 对象中的 数字属性称为可索引属性(Elements)字符串属性被称为命名属性(Properties)
  • 可索引属性应该按照索引值大小升序排列,而命名属性根据创建的顺序升序排列。

obj

命名属性三种存储方式

V8 中 命名属性 有三种的不同存储方式:对象内属性(in-object)快属性(fast)慢属性(slow)

  • 对象内属性 保存在对象本身,提供最快的访问速度。
  • 快属性 比对象内属性多了一次寻址时间。
  • 慢属性 与前面的两种属性相比,会 存储属性的完整结构(另外两种属性的结构在隐藏类中描述),速度最慢(慢属性、属性字典、哈希存储说的都是一回事)。

x

隐藏类

  • 实现对象属性的快速存取,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 性能提升技巧:

  1. 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类);
  2. 总是以相同的次序初始化对象成员;
  3. 尽量使用可以用 31 位有符号整数表示的数;
  4. 为数组使用从 0 开始的连续的主键;
  5. 别预分配大数组(比如大于 64K 个元素)到其最大尺寸,令其尺寸顺其自然发展就好;
  6. 别删除数组里的元素,尤其是数字数组;
  7. 别加载未初始化或已删除的元素;
  8. 对于固定大小的数组,使用 "array literals" 初始化(初始化小额定长数组时,用字面量进行初始化);
  9. 小数组(小于 64k)在使用之前先预分配正确的尺寸;
  10. 请勿在数字数组中存放非数字的值(对象);
  11. 尽量使用单一类型(monomorphic)而不是多类型(polymorphic)(如果通过非字面量进行初始化小数组时,切勿触发类型的重新转换);
  12. 不要使用 try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);
  13. 在优化后避免在方法中修改隐藏类。

References

问题:浏览器 V8 是如何工作的?