说明
图解 Google 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 就可以根据隐藏类中描述的偏移地址获取对应的属性值,这样就省去了复杂的查找流程。
两个假设
隐藏类是建立在两个假设基础之上的:
对象创建好了之后就不会添加新的属性;
对象创建好了之后也不会删除属性。
一旦对象的形状发生了改变,这意味着 V8 需要为对象重建新的隐藏类,这就会带来效率问题。
隐藏类是怎么工作的?
先看下面代码
let point = { x:100, y:200 }
当 V8 执行到这段代码时,会先为 point 对象创建一个隐藏类。
隐藏类描述了对象的属性布局:
point 对象和 map 之间的关系:
有了 map 之后,当你使用 point.x 访问 x 属性时,V8 会查询 point 的 map 中 x 属性相对 point 对象的偏移量,然后将 point 对象的起始位置加上偏移量,就得到了 x 属性的值在内存中的位置,有了这个位置也就拿到了 x 的值,这样就省去了一个比较复杂的查找过程。
实战1:通过 v8-debug 查看隐藏类
可以看看这篇文章:V8 编译浅谈,下图来自这篇文章的截图,里面就提到了一个命令:允许在源代码中使用 V8 提供的原生 API 语法 --allow-natives-syntax
也可以自己去 v8-debug-hlep.txt
的文档里去找:
下面在 kaimo.js
文件里添加下面代码:
let kaimo = { x:100, y:200 }; %DebugPrint(kaimo);
然后在控制台输入命令,可以打印出 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
可以看到,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
我们可以看到:打印出来的 kaimo666
和 kaimo777
对象,它们的 map 属性都指向了同一个地址 0x017100287a59
,说明它们共用了同一个 map。
实战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;
记得加分号,不然会报错:
执行下面命令:
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]) }
利用隐藏类性能优化
避免进行重新构建隐藏类的方法:
- 使用字面量初始化对象时,要保证属性的顺序是一致的。
- 尽量使用字面量一次性初始化完整对象属性。
- 尽量避免使用 delete 方法。