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

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

说明

图解 Google V8 学习笔记



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


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

比如下面例子:

b82c4053931f41afbf65f03e8fd892d7.png


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


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



什么是隐藏类 (Hidden Class)?


V8 引入隐藏类的动机

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



V8 怎么优化 JavaScript 中的对象

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


   对象中所包含的所有的属性;

   每个属性相对于对象的偏移量。


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



两个假设


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


   对象创建好了之后就不会添加新的属性;


   对象创建好了之后也不会删除属性。


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



隐藏类是怎么工作的?


先看下面代码


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




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

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


6e56abdc4b65425cbe4d0f0aded54cd1.png


point 对象和 map 之间的关系:


afe055cb98c046abad668232f8f47487.png



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



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


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

cc4b3dbbe9d34f21ab3d51e73d5724c8.png


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


f367366118fa419d96fae975d5d1e9c6.png

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


ca9a8e16202e448eb7f81374bb3100a6.png



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


2d7717d1d55848bc80d61ecf55662b0d.png


然后在控制台输入命令,可以打印出 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



64770271068d42e9acc4e7c60fc67f8d.png

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



多个对象共用一个隐藏类


什么是对象的形状相同?


要满足以下两点:


   相同的属性名称;


   相等的属性个数。


复用同一个隐藏类的好处


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


   减少隐藏类的创建次数,也间接加速了代码的执行速度;


   减少了隐藏类的存储空间。



实战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



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


f09bbcf1e75d4562be5e821e7c1fa7ac.png


实战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 都是不一样的,分别是 0x006c002823010x006c00287a310x006c00287a59

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

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


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

6bca9649b6ef49608b850377c29a2777.png


执行下面命令:

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


结果如下,我们可以看到 map 也不一样了,分别是 0x006000287a590x006000285709,如果你删除了对象的某个属性,对象的形状也就随着发生了改变,这时 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 方法。





目录
相关文章
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
41 2
|
2月前
|
存储 算法 安全
【JVM】深入理解JVM对象内存分配方式
【JVM】深入理解JVM对象内存分配方式
30 0
|
4月前
|
存储 C++
|
3月前
|
索引
Google Earth Engine(GEE)——提取指定矢量集合中的NDVI值并附时间属性
Google Earth Engine(GEE)——提取指定矢量集合中的NDVI值并附时间属性
39 2
|
3月前
|
Web App开发 编解码 定位技术
Google Earth Engine(GEE)——土壤属性Soil Properties 800m分辨率
Google Earth Engine(GEE)——土壤属性Soil Properties 800m分辨率
18 0
|
3月前
|
NoSQL 数据库
Google Earth Engine(GEE)——美国大陆(CONUS)30米土壤属性概率图数据库
Google Earth Engine(GEE)——美国大陆(CONUS)30米土壤属性概率图数据库
32 0
|
22天前
|
缓存 Java
Java中循环创建String对象的内存管理分析
Java中循环创建String对象的内存管理分析
22 2
|
2月前
|
Python
Python中如何判断两个对象的内存地址是否一致?
Python中如何判断两个对象的内存地址是否一致?
18 0
|
2月前
|
存储 安全 Java
【JVM】Java堆 :深入理解内存中的对象世界
【JVM】Java堆 :深入理解内存中的对象世界
56 0
|
2月前
|
存储
Google Gemini 对于 CL_ABAP_CONV_IN_CE 类中的 UCCP 方法解释,完全不能看
Google Gemini 对于 CL_ABAP_CONV_IN_CE 类中的 UCCP 方法解释,完全不能看
24 0