v8 相关学习
v8 elements kinds (opens in a new tab)
v8 中是如何处理数组元素的
在 JavaScript 中,对象的属性名称可以是任意的字符,对于数值型的(numerically)的 key,v8 对其有特殊的优化,和属性不同,他们的值(元素)被放在另一个空间存储
常规的元素类型:
const array = [1, 2, 3];
// elements kind: PACKED_SMI_ELEMENTS SMI -> small integer
array.push(4.56);
// elements kind: PACKED_DOUBLE_ELEMENTS
array.push("x");
// elements kind: PACKED_ELEMENTS
数组的元素一旦从具象到抽象(比如从 smi 到 regular element),是不会再回去的
PACKED
vs. HOLEY
kinds:
- 连续紧密(dense)的数组:得到更好的优化
- 有洞的数组:可以从 packed 数组转变
The elements kind lattice:lattice 类似网格框架,意思是从具象 -> 抽象 & 紧密 -> 稀疏这两个维度,都是单向转变的,一旦变化之后,v8 是不会回退的(即使把 double 类型都改回 int),根据这几个维度的排列组合,目前一共有 21 种元素类型(21 different elements kinds (opens in a new tab)),对应都有不同的优化手段,越具象的类型能够得到更细粒度的优化,越往 lattice 下面的,优化就越少。所以尽可能的不要改变数组的元素类型
性能优化的 tips:
-
Avoid reading beyond the length of the array:不要越界的去查询数组元素,因为这样会引起原型链的搜索,性能开销是很大的(猜测是在 element 空间找不到之后,还回去 property 空间 + 原型链遍历搜索,如果数组元素本来就很大 or 原型链上很大,那这个遍历开销也会非常大)
-
Avoid elements kind transitions:尽可能的是单一的元素类型,可以在修改数组的时候进行 normalization
const array = [3, 2, 1, +0]; // PACKED_SMI_ELEMENTS array.push(-0); // PACKED_DOUBLE_ELEMENTS const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS array.push(NaN, Infinity); // PACKED_DOUBLE_ELEMENTS NaN 和 Infinity 都会被标记为 double 类型
-
Prefer arrays over array-like objects:创建了一个 array-like 的对象(length,和数字属性)也没有 Array 对象的一些遍历方法,在使用
Array.prototype.forEach.call
的时候实际上会比属性方法调用forEach
效率更差const logArgs = (...args) => { args.forEach((value, index) => { console.log(`${index}: ${value}`); }); }; logArgs("a", "b", "c"); // This logs '0: a', then '1: b', and finally '2: c'. // 这个例子是想说明 ES6 的 rest parameters 可以更好的代替 arguments 对象(别用啦)
-
Avoid polymorphism:避免数组的多态性,文中用
each
这个例子说明了 v8 会对同一个函数接受的数组类型进行 IC(inline-cache),不同元素类型的数组会导致调用时的额外开销,如果相同则可以通过 IC 进行代码复用(生成码) -
Avoid creating holes:数组有 hole 实际上在生产中并不会造成很大的性能损失!尽可能的按照 literal 的方式声明,而不是用
new Array
,因为他一开始就被定义为 holey 数组,并且不可逆转const array = new Array(3); // The array is sparse at this point, so it gets marked as // `HOLEY_SMI_ELEMENTS`, i.e. the most specific possibility given // the current information. array[0] = "a"; // Hold up, that’s a string instead of a small integer… So the kind // transitions to `HOLEY_ELEMENTS`.
文章最后给了如何 debug 类型元素的方法
v8 shape inline cache (opens in a new tab)
视频来自 JS Conf EU 2018
Chrome、Firefox、Edge、Safari 都有各自的 js engine
还是聚焦于 js 的对象,js 引擎可以通过对象的“形状(shape)”,也叫 hidden class
对象的基本操作就是访问属性了,在引擎层是一个寻址的过程,多个结构相同的对象之间可以共享一个 shape,这样就能节省很多空间
transition chain:当对象的结构(shape)发生变化时,会通过一个链式的结构去记录增量的变化,每个结点会指向他前一次变化的节点,每一个节点都包含了新增的属性和他们所在的 offset,通过这个 offset 就可以寻址到 value
transition tree:相同结构开始的不同对象,分别增加不同的属性,构造一棵树的 transition chain
属性访问过程:
a = {x: 1}; a.x = 6; b = {p: 12, x: 3};
这样的情况,其实就有两颗根节点({x}
和 {x, p}
)
当继续添加 b.y = 4; b.z = 3
b 的 chain 就会从 {x, p} -> {y} -> {z}
,同理 a 也增加 z 和 y 和 p 属性 {x} -> {y} -> {z} -> {p}
,访问 a.z
的时候,就从 p 一路向上寻找到 z,其实会比 b.z
访问多一次,也就慢一些
访问 b.z
:
- 找到 b 对象对应的 hiddenClass(shape)
- 在 shape 上寻找他的 z(back chain look up)
- 找到 z 属性的 offset 信息
- 通过 offset 找到对应的 value
inline-cache:记忆从哪里去寻找 js 对象属性的信息,不用每次都进行很复杂的向上寻找
- 对于 Object:根据相同的 shape 缓存每个属性的 offset 信息,直接寻址
- 对象的 shape 变化,就会导致 IC 失败,需要重新进行缓存
- 对于 Array:之前也学到过数组的 shape 会有
length
属性,其元素是放在 elements store 去存储的,不会存储属性值- 如果用了
Object.defineProperty
在数组上(别这么做!),Elements store 实际上会变成一个 dictionay,key 是数组下标,value 是对应元素(常规的属性,enumable/configurable/writable)
- 如果用了
视频也只是对 hiddenClass 和 inline cache 的概念做一个初步的介绍,并且给出了一些编码建议(基于 vm 优化特性):
- 让 js 对象的 shape(结构)尽可能保持不变,让 hiddenClass 保持不变
Object.defineProperty
别在数组上使用
BTW 视频还是非常不错的!
v8 更快的访问 super 属性 (opens in a new tab)
super 关键字可以访问 class 的父类上的属性,依旧是用了 IC(inline cache)(还得去详细学习下)
class 继承的最根本基础还是原型链!
class A {}
A.prototype.x = 100;
class B extends A {
m() {
return super.x;
}
}
const b = new B();
b.m();
这里的 B 继承 A,所以 B.prototype.__proto__
指向 A.prototype
,b 是 B 的实例所以 b.__proto__
指向 B.prototype
,执行 m()
寻找 super.x
的过程就是
- 从 home object(这里就是 m 所定义的对象
B.prototype
),目标就是让访问super.x
的过程变得更快! - 这个 case 中,x 是很快就能被访问到的,但是很多情况可能需要 look up 通过很长的 prototype chain 才能寻找到,此时就需要用 IC 进行加速
- 另说一下,这里即使
B.prototype
有x
也不会去找的,因为super
是从 home object 的__proto__
(也就是B.prototype.__proto__
)去找,receiver 就是访问 super 函数的调用者(receiver) - 实现细节:
- Ignition (opens in a new tab) bytecode,
LdaNamedPropertyFromSuper
, a new IC,LoadSuperIC
, for speeding up super property loads. LoadSuperIC
reuses the existing IC machinery for property loads, just with a different lookup start object.- 具体代码在
JSNativeContextSpecialization::ReduceNamedAccess
(opens in a new tab),chromium 项目的在线编辑器,搜索代码比较方便(虽然看不懂代码)
- Ignition (opens in a new tab) bytecode,
- 最后有一些场景可能是 vm 优化不到的,比如直接给
super.x = ...
修改了。或者用 mixin 方式会把 inline cache 变成全局的 cache (megamorphic)就慢了点
v8 对象属性访问 (opens in a new tab)
V8 是如何处理动态新增的属性,让其的访问也能快速
named properties(具体的 key)和元素(数组,整数下标)
两者存储的形式类似,都是连续的数组 or 字典,两者的存储是独立分开的
JS Object
- hiddenClass
- properties
- elements
HiddenClass
- bit field 1 // 并不是 1 bit 而是一种结构体
- bit field 2
- bit field 3 // 这个结构中存了属性的数量 和 descriptor array 的指针
HiddenClass:meta data,存储了一个对象的形状信息(shape)以及属性名到下标的映射
- 属性的数量
- 指向原型的引用
- 随着对象的变化会动态更新
- 相同构造的对象共享同一个 HiddenClass
- V8 内部用一个 transition tree 来连接所有 HiddenClass,这样只要是按照一样的属性增加的顺序,最终得到的 HiddenClass 也是同一个(类似状态机流转状态,单向图),每次新增一个属性都会创建一个新的 hiddenClass
- 增加数组的下标属性不会增加 hiddenClass,因为是存在另一个独立的 elemenst 区域
- 指向 descriptor 数组记录了属性名和位置,以及值存在哪里
三种属性类型
in-object 属性 vs. 普通属性
- in-object 是 v8 访问最快的属性,直接存储在对象内部
- 数量是由对象初始化的结构决定的
- 普通属性:运行时新增的属性被加入到 属性存储区,多一层间接访问
普通属性又分:fast 属性 vs. slow 属性
- fast:在 属性存储区 中可以线性访问的属性(直接通过下标)
- slow:有慢属性的对象有一个内置的字典作为属性存储(不会在 HiddenClass 上共享),属性增加和移除是不会更新 HiddenClass 的,同时 inline cache 也不能作用,所以访问慢,但是增加和删除的效率高(不用改 hiddenClass)
Named Property:
1. in-object -> directly on the obj
2. Fast -> properties store; meta info -> descriptor array(HiddenClass)
3. Slow -> properties dictionary
对于数组元素
如果数组中间有 hole [1,,3]
这样,对于下标 1 的访问会去 prototype 上的 elements 找,对于 elements 来说,也是 self-contained 的,不会在 HiddenClass 上共享
如果知道数组对象上没有 hole 就能认为是 packed 的,能够提升访问效率(不会去原型上找)
有 20 种数组元素类型 (opens in a new tab)。。。为的是 VM 可以根据特定的类型进行访问的加速。
Fast or Dictionary 元素:
- Fast:简单的 VM 数组结构
- Slow:稀疏数组会通过字典来节省内存
const sparseArray = [];
sparseArray[9999] = "foo"; // Creates an array with dictionary elements.
Smi and Double Elements
- Smi(Small Integers),纯整数数组,整数是直接 encode 在数组中的,不会经历 GC
- Double,纯浮点数数组 V8 stores raw doubles for pure double arrays to avoid memory and performance overhead
const a1 = [1, 2, 3]; // Smi Packed
const a2 = [1, , 3]; // Smi Holey, a2[1] reads from the prototype
const b1 = [1.1, 2, 3]; // Double Packed
const b2 = [1.1, , 3]; // Double Holey, b2[1] reads from the prototype
每一种类型其实都通过 C++ 实现的 ElementsAccessor(基于 CRTP (opens in a new tab)),没有深入了解。简单来说像是一个代理,决定是哪种类型的数组。
知道如何访问属性是在 V8 中优化的关键,可以知道为什么某些代码写出来就是快!
v8 hash code (opens in a new tab)
v8 官方 blog
ES 2015 引入了一些新的数据结构比如 Map Set WeakSet WeakMap,这些底层其实都是用 hash table 实现的。这篇博文介绍了
- Hash Code 是什么:
- hash function 将一个 key 映射成 hash table 中的一个位置(下标、...)
- hash code 就是 hash function 执行之后的结果
- V8 中 hash code 就是一个随机的数字,独立于对象,必须存起来(每个对象可以有一个)
- 是对象一个类似
Symbol
的 privite key,但是不会暴露给用户侧的 js - 并且这个 hash code 是当对象需要它时才会计算和存储,不用到的时候可以节省空间
- V8 优化查找这个 hash code 的方式是一样的用 monomorphic IC lookups,inline-cache!(当对象有相同的 hidden class),但是大多数情况都不能满足,就会 megamorphic IC lookups(可以理解是全局的 cache?比较慢了)
- 访问这个 prvite symbol 也会触发 hidden class transition
- JS Object 背后如何存数据的
- one word for storing a pointer to the elements backing store, and another word for storing a pointer to the properties backing store.
- elements:就是数组的元素,在内部也是类似数组的结构
- properties:属性值,string or symbols
- one word for storing a pointer to the elements backing store, and another word for storing a pointer to the properties backing store.
- 如何存(hide) hash code
- 存在 elements,因为数组是不定长,总会浪费空间
- 所以会存在 properties 的空间:数组 or 字典
- 空。无 properties
- array(最大限制 1022 个,超过后 V8 会转成 dictionary 存)
- dictionary(会新开辟一个空间,但是问题不大)
- 三种方式存储之后,得到的结果是:hash code 的 lookup 不需要和 js 对象属性访问那么复杂了!
小结:
- Map 为什么能比对象取 key 更快?就是因为读取的是元素的 hash code,hash code 又通过上述存储方式可以比常规属性访问快速很多!
- 个人假想:
Map.set(key, value)
的时候,是先获取 key 的 hash code,将 value 存在 hash table,get 取的时候也直接取 key 的 hash code(很快),所以 Map 的存取操作非常快 O(1)。任何字面量/常量的 hash code 应该也是一样的?或者说存储的地方也是同一个,保证getHash(true) === 'xxxx'
- 个人假想:Map 的 key 为啥是有序的,内部通过数组来存的 key 的引用?Remained Problem