0
点赞
收藏
分享

微信扫一扫

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?


说明

图解 Google V8 学习笔记

为什么静态语言的效率更高?

静态语言中,可以直接通过偏移量查询来查询对象的属性值。

比如下面例子:

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_v8


JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少。V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。

C++ 代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的。编译器会直接将 x 相对于 start 的地址写进汇编指令中,使用了对象 start 中的 x 属性时,CPU 直接去内存地址中取出该内容即可,没有任何中间的查找环节。

什么是隐藏类 (Hidden Class)?

V8 引入隐藏类的动机

因为 JavaScript 是一门动态语言,对象属性在执行过程中是可以被修改的,这就导致了在运行时,V8 无法知道对象的完整形状,那么当查找对象中的属性时,V8 就需要经过一系列复杂的步骤才能获取到对象属性。

V8 怎么优化 JavaScript 中的对象

V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息:

  • 对象中所包含的所有的属性;
  • 每个属性相对于对象的偏移量。

在 V8 中,把隐藏类又称为 map,每个对象都有一个 map 属性,其值指向内存中的隐藏类。有了隐藏类,V8 就可以根据隐藏类中描述的偏移地址获取对应的属性值,这样就省去了复杂的查找流程。

两个假设

隐藏类是建立在两个假设基础之上的:

  1. 对象创建好了之后就不会添加新的属性;
  2. 对象创建好了之后也不会删除属性。

一旦对象的形状发生了改变,这意味着 V8 需要为对象重建新的隐藏类,这就会带来效率问题。

隐藏类是怎么工作的?

先看下面代码

let point = {
x:100,
y:200
}

当 V8 执行到这段代码时,会先为 point 对象创建一个隐藏类。

隐藏类描述了对象的属性布局:

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_javascript_02

point 对象和 map 之间的关系:

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_v8_03

有了 map 之后,当你使用 point.x 访问 x 属性时,V8 会查询 point 的 map 中 x 属性相对 point 对象的偏移量,然后将 point 对象的起始位置加上偏移量,就得到了 x 属性的值在内存中的位置,有了这个位置也就拿到了 x 的值,这样就省去了一个比较复杂的查找过程。

实战1:通过 v8-debug 查看隐藏类

可以看看这篇文章:​​V8 编译浅谈​​​,下图来自这篇文章的截图,里面就提到了一个命令:允许在源代码中使用 V8 提供的原生 API 语法 ​​--allow-natives-syntax​

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_偏移量_04


也可以自己去 ​​v8-debug-hlep.txt​​ 的文档里去找:

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_v8_05

下面在 ​​kaimo.js​​ 文件里添加下面代码:

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_javascript_06

let kaimo = {
x:100,
y:200
};
%DebugPrint(kaimo);

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_javascript_07

然后在控制台输入命令,可以打印出 kaimo 对象的基础结构:

v8-debug --allow-natives-syntax kaimo.js

kaimo 的内存结构如下:

DebugPrint: 00000032000CA015: [JS_OBJECT_TYPE]
- map: 0x003200287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x003200244215 <Object map = 00000032002821E9>
- elements: 0x003200002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x003200002261 <FixedArray[0]>
- All own properties (excluding elements): {
0000003200253589: [String] in OldSpace: #x: 100 (const data field 0), location: in-object
0000003200253599: [String] in OldSpace: #y: 200 (const data field 1), location: in-object
}
0000003200287A59: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x003200287a31 <Map[20](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0032001c4581 <Cell value= 1>
- instance descriptors (own) #2: 0x0032000ca045 <DescriptorArray[2]>
- prototype: 0x003200244215 <Object map = 00000032002821E9>
- constructor: 0x003200243e29 <JSFunction Object (sfi = 00000032001DB6D9)>
- dependent code: 0x0032000021e9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_v8_08

可以看到,kaimo 对象的第一个属性就是 map,它指向了 ​​0x003200287a59​​ 这个地址,这个地址就是 V8 为 kaimo 对象创建的隐藏类,除了 map 属性之外,还有 prototype 属性,elements 属性和 properties 属性。

多个对象共用一个隐藏类

什么是对象的形状相同?

要满足以下两点:

  1. 相同的属性名称;
  2. 相等的属性个数。

复用同一个隐藏类的好处

如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,好处:

  1. 减少隐藏类的创建次数,也间接加速了代码的执行速度;
  2. 减少了隐藏类的存储空间。

实战2:多个对象共用一个隐藏类

替换 ​​kaimo.js​​ 的代码,

let kaimo666 = {
x: 6,
y: 66
};
let kaimo777 = {
x: 7,
y: 77
};
%DebugPrint(kaimo666);
%DebugPrint(kaimo777);

然后再执行下面命令:

v8-debug --allow-natives-syntax kaimo.js

输出结果如下:

DebugPrint: 00000171000CA05D: [JS_OBJECT_TYPE]
- map: 0x017100287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x017100244215 <Object map = 00000171002821E9>
- elements: 0x017100002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x017100002261 <FixedArray[0]>
- All own properties (excluding elements): {
0000017100253589: [String] in OldSpace: #x: 6 (const data field 0), location: in-object
0000017100253599: [String] in OldSpace: #y: 66 (const data field 1), location: in-object
}
0000017100287A59: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x017100287a31 <Map[20](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0171001c4581 <Cell value= 1>
- instance descriptors (own) #2: 0x0171000ca08d <DescriptorArray[2]>
- prototype: 0x017100244215 <Object map = 00000171002821E9>
- constructor: 0x017100243e29 <JSFunction Object (sfi = 00000171001DB6D9)>
- dependent code: 0x0171000021e9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

DebugPrint: 00000171000CA0B5: [JS_OBJECT_TYPE]
- map: 0x017100287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x017100244215 <Object map = 00000171002821E9>
- elements: 0x017100002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x017100002261 <FixedArray[0]>
- All own properties (excluding elements): {
0000017100253589: [String] in OldSpace: #x: 7 (const data field 0), location: in-object
0000017100253599: [String] in OldSpace: #y: 77 (const data field 1), location: in-object
}
0000017100287A59: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x017100287a31 <Map[20](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0171001c4581 <Cell value= 1>
- instance descriptors (own) #2: 0x0171000ca08d <DescriptorArray[2]>
- prototype: 0x017100244215 <Object map = 00000171002821E9>
- constructor: 0x017100243e29 <JSFunction Object (sfi = 00000171001DB6D9)>
- dependent code: 0x0171000021e9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

我们可以看到:打印出来的 ​​kaimo666​​​ 和 ​​kaimo777​​​ 对象,它们的 map 属性都指向了同一个地址 ​​0x017100287a59​​,说明它们共用了同一个 map。

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_偏移量_09

实战3:重新构建隐藏类

给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么就会触发 V8 为改变形状后的对象重建新的隐藏类。

在 ​​kaimo.js​​ 替换下面代码:

let kaimo = {};
%DebugPrint(kaimo);
kaimo.x = 666;
%DebugPrint(kaimo);
kaimo.y = 666;
%DebugPrint(kaimo);

执行下面命令:

v8-debug --allow-natives-syntax kaimo.js

结果如下:

DebugPrint: 0000006C000CA045: [JS_OBJECT_TYPE]
- map: 0x006c00282301 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x006c00244215 <Object map = 0000006C002821E9>
- elements: 0x006c00002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x006c00002261 <FixedArray[0]>
- All own properties (excluding elements): {}

DebugPrint: 0000006C000CA045: [JS_OBJECT_TYPE]
- map: 0x006c00287a31 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x006c00244215 <Object map = 0000006C002821E9>
- elements: 0x006c00002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x006c00002261 <FixedArray[0]>
- All own properties (excluding elements): {
0000006C002535A1: [String] in OldSpace: #x: 666 (const data field 0), location: in-object
}

DebugPrint: 0000006C000CA045: [JS_OBJECT_TYPE]
- map: 0x006c00287a59 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x006c00244215 <Object map = 0000006C002821E9>
- elements: 0x006c00002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x006c00002261 <FixedArray[0]>
- All own properties (excluding elements): {
0000006C002535A1: [String] in OldSpace: #x: 666 (const data field 0), location: in-object
0000006C002535B1: [String] in OldSpace: #y: 666 (const data field 1), location: in-object
}

可以看到,3个 map 都是不一样的,分别是 ​​0x006c00282301​​​,​​0x006c00287a31​​​,​​0x006c00287a59​​。

再来看看删除的情况,在 ​​kaimo.js​​ 替换成下面的代码:

let kaimo = {
x: 666,
y: 777
};
%DebugPrint(kaimo);
delete kaimo.x;
%DebugPrint(kaimo);

这里的 ​​delete kaimo.x;​​ 记得加分号,不然会报错:

图解 Google V8 # 15:隐藏类:如何在内存中快速查找对象属性?_javascript_10

执行下面命令:

v8-debug --allow-natives-syntax kaimo.js

结果如下,我们可以看到 map 也不一样了,分别是 ​​0x006000287a59​​​ 跟 ​​0x006000285709​​,如果你删除了对象的某个属性,对象的形状也就随着发生了改变,这时 V8 也会重建该对象的隐藏类。

DebugPrint: 00000060000CA03D: [JS_OBJECT_TYPE]
- map: 0x006000287a59 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x006000244215 <Object map = 00000060002821E9>
- elements: 0x006000002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x006000002261 <FixedArray[0]>
- All own properties (excluding elements): {
0000006000253589: [String] in OldSpace: #x: 666 (const data field 0), location: in-object
0000006000253599: [String] in OldSpace: #y: 777 (const data field 1), location: in-object
}

DebugPrint: 00000060000CA03D: [JS_OBJECT_TYPE]
- map: 0x006000285709 <Map[12](HOLEY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x006000244215 <Object map = 00000060002821E9>
- elements: 0x006000002261 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0060000ca095 <NameDictionary[29]>
- All own properties (excluding elements): {
y: 777 (data, dict_index: 2, attrs: [WEC])
}

利用隐藏类性能优化

避免进行重新构建隐藏类的方法:

  1. 使用字面量初始化对象时,要保证属性的顺序是一致的。
  2. 尽量使用字面量一次性初始化完整对象属性。
  3. 尽量避免使用 delete 方法。

举报

相关推荐

0 条评论