JavaScript的稀疏数组和密集数组

文章首发:github.com/qiudongwei/…git

测试一段代码

运行环境:Chrome JavaScript | V8 8.9.255.20github

let array1 = [1];
let array2 = [1];
// array2[2]=2; // 超出数组长度

console.time('array1');  
for (let i = 0; i < 10000000; i++) {  
    array1.push(i); 
}  
console.timeEnd('array1'); 


console.time('array2');  
for (let j = 0; j < 10000000; j++) {  
    array2.push(j); 
}  
console.timeEnd('array2'); 
复制代码

① 测试几回发现,array1和array2耗时相近,几乎是同样的; ② 取消第三行的注释再运行,测试发现,array1的耗时明显要小于array2;数组

或者直接在这测试:jsben缓存

这是为何呢?V8内部是如何处理数组操做的?性能优化

数组中的元素类型

在JavaScript层面,开发者能够给数组的元素设置任意基础数据类型,eg:arr = [1, 1.2, 'hello', {}, []],然而,在V8层面,它理解的元素类型却只有三种:markdown

  • SMI_ELEMENTS:又称Smi,小整数(Small integers)
  • DOUBLE_ELEMENTS:双精度浮点数,浮点数和不能表示为 Smi 的整数
  • ELEMENTS:常规元素,不能表示为 Smi 或双精度的值

举个例子,暂时忽略PACKED_这个前缀:app

const arr = [1, 2, 3]; // 元素类型: PACKED_SMI_ELEMENTS
arr.push(1.54);         // 元素类型变为: PACKED_DOUBLE_ELEMENTS
arr.push('c');            // 元素类型变为: PACKED_ELEMENTS
复制代码

由上例子可知,V8为每一个数组分配一种元素类型 SMI_ELEMENTS | DOUBLE_ELEMENTS | ELEMENTS,而且元素类型的转换只能从特定的(eg:SMI_ELEMENTS )到更通常的(eg:ELEMENTS)。函数

再看一个例子:oop

const arr = [1, 2, ,4]; // 元素类型HOLEY_SMI_ELEMENTS
arr.push(5);               // 元素类型变为: HOLEY_DOUBLE_ELEMENTS
arr.push('a');             // 元素类型变为: HOLEY_ELEMENTS
复制代码

这个例子里面,arr出现了个洞(没有被完满填充)咱们称其为稀疏数组,用一个HOLEY去标识其类型; PACKED则表示这是个密集数组。 稀疏数组是不可逆的,一旦标记为有空,它就是永远有洞,即使它被填充了!稀疏数组不可转变为密集数组,反之却能够。以下图代表了各元素类型之间的转换关系: image性能

而数组的稀疏与否,有可能(数据量足够大的时候)会影响数组操做的性能。

数组的性能问题

  1. 对于稀疏数组,V8须要对其原型链进行额外的检查和昂贵的查找;
  2. 通常来讲,更具体的元素种类能够进行更细粒度的优化。元素类型越通常,对该对象的操做就会越慢;
  3. V3依赖其内嵌缓存(inline cache)机制能够更有效地去对具体类型的(密集)数组做数组操做的优化;

避免建立稀疏数组

  1. 避免new Array建立数组
// bad
const arr = new array(5);

// good
const arr = [0, 0, 0, 0, 0];

// better
const arr = Array.from({length: 5}
复制代码
  1. 避免越界访问数组
const arr = [1, 2, 3];

// bad
log(arr[3]);
复制代码
  1. 避免给length赋值
const arr = [1, 2, 3];

// bad
arr.length = 5;
复制代码

数组多态

若是您的代码须要处理包含多种不一样元素类型的数组,则可能会比单个元素类型数组要慢,由于你的代码要对不一样类型的数组元素进行多态操做。看这个例子:

const each = (array, callback) => {
  for (let index = 0; index < array.length; ++index) {
    const item = array[index];
    callback(item);
  }
};

// 第一次调用
each(['a', 'b', 'c'], doSomething);

// 第二次调用
each([1.1, 2.2, 3.3], doSomething);

// 第三次调用
each([1, 2, 3], doSomething);
复制代码

在第一次调用中,数组类型是 PACKED_ELEMENTS,V8使用 inline cache(内嵌缓存)记住 each 函数是被一个特定元素类型调用的,在下一次调用时,V8会先检查数组类型是不是 PACKED_ELEMENTS,若是是,则复用上次生成的代码,不然会作更多的事情,如从新生成相应的代码;

第二次调用的时候,数组类型是 PACKED_DOUBLE_ELEMENTS,V8检查到出现了与缓存中不同的数组类型,此时,V8将 each 函数中的数组标记为多态,接下来的每一次 each 调用都须要进行 PACKED_ELEMENTSPACKED_DOUBLE_ELEMENTS 两种类型的检查,这会形成一些性能消耗;

第三次调用结束的时候,V8缓存中出现了3种不一样类型的 each 缓存,V8为了可以复用生成的代码,接下来的每一次 each 调用,都会作相应的类型检查。

然而,内置方法(eg:Array.prototype.forEach)能够更有效地处理这种多态性,所以在性能敏感的状况下应用优先考虑使用它。

参考:

V8 中的元素种类及性能优化

相关文章
相关标签/搜索