V8对数组的优化
在 c++、java 中的数组的特点是:是通过在内存中划分一串连续的、固定长度的空间。来存放一组有限的并且是相同类型的数据结构。
js中的数组
var arr = [100, 12.3, 'a', function () {return 1}, {a: 1}];
arr[arr.length] = '12334';
arr.length = 1;
js 中的数组可以存放任意的类型。 可以动态的来给数组修改长度。
- 支持任意的类型。
- js数组可以动态的的改变容量,根据数组的数据来扩容、收缩。
- js提供了很多的操作数组的方法。
- js数组不是基础的数据结构实现的,而是在基础上做了一些封装。
V8中看数组
JSArray 继承自 JSObject, 数组是一个特殊的对象。
本质上数组也是一个对象,内部也是 key-value 的存储形式。
底层是一个 Map,key 为0,1,2,3索引。 index 为字符串。
数组有两种表现形式, fast和slow。
fast(快数组 FAST ELEMENTS)
快数组是一种线性的存储方式。新创建的空数组,默认的存储方式是快数组,快数组长度是可变的,可以根据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现。
- 扩容
新容量的计算方式: 扩容后的新容量 = 旧容量 * 1.5 + 16;
扩容后会将数组拷贝到新的内存空间中。
- 扩容
var arr = [1, 2];
// 扩容后的新容量 = 旧容量 * 1.5 + 16;
arr[arr.length] = 3;
/*
新容量 = 2 * 1.5 + 16;
3 + 16 = 19;
*/
- 收缩
如果容量 > length * 2 + 16, 旧进行收缩容量。 否则就调用 holes 对象来填充未被初始化的位置。
收缩的大小:
int elements_to_trim = length + 1 == old_length ? ... : ...;
根据 length +1 和 old_length 来判断,是将空出的内容全部收缩还是只是收缩一半。
holes(空洞) 对象指的是数组中分配了空间,但是如果没有存放元素的位置。这个模式叫 Fast Holey Element 模式。
- 收缩
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
arr.length = 1;
// 如果 容量 > length * 2 + 16
// 否则 用 holes 对象填充未被初始化的位置
/*
20 > 1 * 2 + 16
将要进行收缩
*/
// 那么是将空出的空间全部收缩还是收缩二分之一
length + 1 == old_length ? 空出空间 / 2: 空出空间;
1 + 1 == 20 ? 空出空间/2 : 空出空间。
所以: 这次操作会将空出来的空间全部收回。
目前容量 = 1;
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
arr.length = 2;
14 > 2 * 2 + 16 不成立
使用 holes 对象填充位被初始化的位置
最终容量展示: [1, 2, empty * 12];
- 新建数组时,如果没有设置容量,V8会默认使用 Fast Elements 模式实现。 如果要对数组设置容量,但是没有进行内部元素的初始化, new Array(10) 数组就存在了空洞,就会以 Fast Holey Elements 模式实现。
slow(慢数组)
慢数组是一种字典内存形式。不用开辟大的连续的内存空间,节省空间。但是需要维护一个 HashTable。效率比快数组低。
hasTable: 散列表,是根据 key 来直接访问在内存存储位置的数据结构。通过计算一个关于键值的函数,将所需要查询的数据映射到表中一个位置来访问记录,加快了查找速度。
fast 和 slot 区别
-
存储方式方面:快数组内存中连续的,慢数组在内存中零散分配。
-
内存使用方面:快数组内存是连续的,需要开辟一块大的内存供使用,可能会浪费较多的内存空间。慢数组不会有空洞的情况, 都是零散的内存,比较节省内存空间。
-
遍历效率:快数组由于空间连续,遍历速度快。慢数组每次都要寻找 key 的位置,遍历效率会差一点。
-
数组的存储结构是有变化的,会根据不同的情况将快慢数组转换。
-
快速组/慢数组
console.time('time');
const arr = new Array(1030);
console.log(arr[1029]);
console.timeEnd('time');
console.time('time2');
var a = [];
for (var i = 0; i < 1030; i++) {
a.push(i);
}
console.log(a[1029]);
console.timeEnd('time2');
// 结果
undefined
time: 6.866ms
1029
time2: 0.607ms
快->慢
新容量 >= 3 * 扩容后的容量 * 2 ,会转为慢数组。
至少有 1024 个空洞,会转变为慢数组。 这个时候对数组分配大量空间可能造成存储空间的浪费,为了空间的优化,会转为慢数组。
- 快数组转为慢数组
console.time('time3');
let a = [1, 2];
a[1030] = 1;
console.log(a[1029]);
console.timeEnd('time3');
慢->快
处于散列表实现的数组,在每次空间增长时,V8的启发算法会检查其空间占用量,若其空洞口减少到一定程度,就会转为快数组模式。
当慢数组的元素可存放在快数组中且长度在 smi 之间且仅节省了50%的空间,就会转为快数组。
- 慢数组转为快数组
console.time('time');
const arr = new Array(1030);
for (var i = 200; i < 1030; i++) {
arr[i] = i;
}
console.log(arr[1029]);
console.timeEnd('time');
// 结构
time: 5.918ms
我们可以看出来,即使慢数组变为快数组, 效率也不一定高。
测试一(测试扩容/收缩容量)
- 扩容
var arr = [1, 2];
// 扩容后的新容量 = 旧容量 * 1.5 + 16;
arr[arr.length] = 3;
/*
新容量 = 2 * 1.5 + 16;
3 + 16 = 19;
*/
- 收缩
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
arr.length = 1;
// 如果 容量 > length * 2 + 16
// 否则 用 holes 对象填充未被初始化的位置
/*
20 > 1 * 2 + 16
将要进行收缩
*/
// 那么是将空出的空间全部收缩还是收缩二分之一
length + 1 == old_length ? 空出空间 / 2: 空出空间;
1 + 1 == 20 ? 空出空间/2 : 空出空间。
所以: 这次操作会将空出来的空间全部收回。
目前容量 = 1;
模式: Fast Elements
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
arr.length = 2;
14 > 2 * 2 + 16 不成立
使用 holes 对象填充位被初始化的位置
最终容量展示: [1, 2, empty * 12];
模式:Fast Holey Elements
测试二(测试快/慢数组,以及快慢数组转换)
- 快速组/慢数组
console.time('time');
const arr = new Array(1030);
console.log(arr[1029]);
console.timeEnd('time');
console.time('time2');
var a = [];
for (var i = 0; i < 1030; i++) {
a.push(i);
}
console.log(a[1029]);
console.timeEnd('time2');
// 结果
undefined
time: 6.866ms
1029
time2: 0.607ms
- 快数组转为慢数组
console.time('time3');
let a = [1, 2];
a[1030] = 1;
console.log(a[1029]);
console.timeEnd('time3');
- 慢数组转为快数组
console.time('time');
const arr = new Array(1030);
for (var i = 200; i < 1030; i++) {
arr[i] = i;
}
console.log(arr[1029]);
console.timeEnd('time');
// 结构
time: 5.918ms
我们可以看出来,即使慢数组变为快数组, 效率也不一定高。