w3ctech

V8 Hidden Class

貘大编写,我负责搬砖

不知道出于什么不可告人的原因 @裕波 要让偶写 V8 引擎实现的 Hidden Class 是怎么回事[lt雷]。可偶是个懒人啊,偶也不会这个啊,肿麽办[熊猫]?!他不是想让偶难堪吧,Hidden Class 这种能跟 EventLoop/AIO 比肩的操作系统深层次原理的东西(你们懂的 [doge] ),偶咋会知道呢,哭[泪]。

好在偶还会 Google 啥的,随便查查,然后拼凑出点内容忽悠过周老板就好了(反正他也不懂 [阴险])。

开始絮叨。

V8 Hidden Class 这东西吧貌似很早就有了,一种引擎实现机制,不知道为啥周董会对这个感兴趣,起码 2009 年时候 Google 就说过这个么[挖鼻屎]。

这玩意偶觉得是对 web 前端没啥用,当然你说对写千万负载的 Node 服务兴许有用,那偶也不反对啦。实际上它就是个类属性收集器(方式)而已,避免每次查找对象属性的 hash 操作。

大概看看 Java 例子(当然这些都是偶Google抄来的[得意地笑])

class Point {
 Point(int x, int y) {
 this.x = x;
 this.y = y;
 }
private:
 int x;
 int y;
}
Point p1 = new Point(10, 11);
Point p2 = new Point(12, 13);

Point 类的两个实例存放方式:

Point 类的两个实例存放方式

好了,这是Java,它可以共享 Class info ,很好理解因为 Java 不是动态脚本,运行时不能为类对象添加属性。它的 Class info 可以在制定内存地址保存固话。成员对象属性先访问到info 表,获取得到属性对应值地址偏移后,通过指针偏移得到属性值。

回过头来看 JS 这货。

它的对象压根就是个哈希表(大字典……)着存呗,存上key和value完事,key被hash,value是值地址。

比如类似 Java 的代码

function Point(x, y) {
 this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

大概是这样:

发现没,Point 的成员属性都是相同的,但是被分散存储了。每个对象取值都要 hash key 然后再找到其 value。

Google 这帮货不爽了就,他们觉得 hash 太慢,要变变,起码变成跟 Java 内样的。

为了这个蛋疼[喵喵]目的于是 JS Object 就变成了这样一组结构:

每个 WORD 是一个32位指针,4字节对齐(4*8 = 32)

地址的最低两位可以用做标记,这样31位带符号整数可以区分标记和指针

(这个其实跟现在说的没啥关系,主要是为了区分密实数组) (V8 对于0开始连续索引数,且元素<64k的采用密实数组形式保存,就是真数组,如果delete某index就会转为散列形式——你懂的,又“慢”呗) 一个 Object 由三个 WORD 构成,分别记录

  • Hidden Class 指针
  • Properites 指针
  • Elements 指针

有了这个结构,为的就是把有相同 Properites 的对象指到同一个 Class info 数据结构上去。这样就免去每次key时的 hash 了,就如同 Java 一般。

看代码执行过程:

function Point(x, y) {
 this.x = x;
 this.y = y;
}
--> var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

先建立了一个 Point 构造函数对应的 Hidden Class —— Class 0。

然后 p1 对象的 Hidden 指针指向它:

function Point(x, y) {
 -->  this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

this.x 赋值时,Class 0 里添加 x 属性指针指向新建的 Class 1 结构,其内存储 x 属性在 Object 内 Properties 指针所指存储地址偏移值,这里假定为 0 好了。根据这个偏移地址,p1 Object 在属性值地址的偏移0位保存 x 的值 10。 (这里的offset仅是示意,用0、1、2、3……表示,实际上根据值类型不同,offset 会更大。比如,例子中的 10 假设为会存储为 int32,那么下次 offset 会到 32。后面的例子子里仅仅用 1 表示已经偏移了)

function Point(x, y) {
 this.x = x;
 --> this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

this.y 赋值时,新建 Class2 ,保留 Class1 值,在Class 1并添加 y 属性指针指向新建的 Class 2 结构,Class 2 开辟空间储 y 属性在 Object 内 Properties 指针所指存储地址偏移值1。 由于这次 new 已经结束,将 Hidden 指针指向最终的 Class 2。

function Point(x, y) {
 this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
-> var p2 = new Point(12, 13);

继续执行到 p2 对象的构建。它是 Point 的实例,Point 本身的 info 已经在 p1 实例化时构建完成。因此仅仅执行一边 Class 0 - 2 的链接指向存放属性值就可以。

先指向 Class 0,知道有属性 x 要添加。

function Point(x, y) {
 -->  this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

然后指向 Class 1,在指定offset 0 处添加值 12。

function Point(x, y) {
 this.x = x;
 --> this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

继续指向 Class 2 添加值 y,并固定 Hidden Class 指针指向 Class 2。

最终获得与 Java 类似的对象数据存储结构,使用指针偏移替代了key 的 hash 计算。

到此,基本上说完 Hidden Class 是什么,怎么整的。

然后呢然后呢然后呢然后呢,这种优化肯定有问题吧[挖鼻屎]。如果构造对象的属性不变更,那么 Hidden Class 机制肯定是快的。但是——JS 对象是可以动态添加移除属性的不是么。 如果是这样的代码,肿么办?

function Point(x, y) {
 this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);
//  →_→ 
p2.z = 14;

这样 Class 2 得 Add z。

之后再来个 Class 3 ……

此时由于 p2 属性修改,导致 p1 与 p2 分属两个 Hidden Class 来控制器属性值偏移。

这只是修改的情况,如果删除了某属性值呢?

function Point(x, y) {
 this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);
//  →_→ 
delete p2.x;

那好,玩不了,老实把所有属性值遍历,乖乖转为哈希表存。这样 p1 是通过指针偏移访问的,p2 就是相对慢的哈希了。

说这么多,其实还有很多情况让V8没法好好使用 Hidden Class 模式构建对象。 比如:

function Point(x, y, z) {
  if (z) { 
   this.x = x;
  } else {
   this.y = y;
 }
}
function Point(x, y, z) {
  if (z) { 
   this.y = x;
  } else {
   this.x = y;
 }
}

类似运行时属性赋值顺序不同,运行时属性缺失等情况都将击破这种优化,退回到哈希表模式。

这还没完,如果你细心,就会发现 Hidden Class 初始时候是针对构造函数的。例子里用的时 Point 函数。实际情况下 {} 字面量和实际上是 new Object(),对 Point 实例构造的 Hidden Class 过程等同于对 Objcet 实例构造过程。所以还存在一种比较容易碰到的优化变拖累情况。

  var t = new Date();
  var max = 1e4;
  for(var i = 0; i < max; ++i) {
    var n = {};
    n['i_' + i] = i;    
  }
  console.log('首次', new Date-t, '===== n =====');

  t = new Date();
  for(var i = 0; i < max; ++i) {
    var m = {};
    m['i_' + i] = i;    
  }
  console.log('重复', new Date-t, '===== m =====');

Chrome 结果可能是(根据机器速度不同数值可能有浮动):

  • 首次 683 ===== n =====
  • 重复 45 ===== m =====
  • 这是 n 构建第一次时,重复构建一批对应 Object 构造函数的 Hidden Class 类(见前 p2.z 设置例子),构建时间被指数级延长。
  • 等到 m 构建时分别可对应到 n 构建后 Object 构造函数的 Hidden Class 类指针,因此构建速度加快。

而 Safari 这样没有采用此优化方式的结果可能是:

  • 首次 34 ===== n =====
  • 重复 30 ===== m =====

好了,叨逼这么多,基本上算是说明白了吧[lt蛋疼]。结贴,跟周董要吃喝去[bm内涵]

w3ctech微信

扫码关注w3ctech微信公众号

共收到4条回复

  • 确实,这样子偏移查询就很快了,可能是顺序表结构的原因查询忒快,但是牺牲的就是改动属性的时候没有hash这种类似链表结构方式的快。不知道理解错没,按代码执行顺序讲解太好啦~

    不过java属性赋值有下面这个写法么o好怪... private: int x; int y;

    回复此楼
  • 不好意思,偶Qt写多了,哇哈哈哈。

    回复此楼
  • ** 貘大的必须挺啊

    回复此楼
  • 请问v8中对应的代码在哪里啊?谢谢。

    回复此楼