/* 原文阅读自前端早读课1071期 */
为什么说JavaScript数组不是真正的数组
数组是用来存储元素的线性集合,在内存中占据一串连续的内存位置。注意重点,“连续”(continuous)。
上图展示了数组在内存中的存储方式,这个数组保存了4个元素,每个元素4个字节,加起来总共占用了16字节的内存区。
假设我们声明了 一个元素全为整数的数组arr[4],分配到的内存区的地址从1201开始。一旦需要读取arr[2],只需要通过数学计算拿到arr[2]的地址即可,计算1201+(2*4),直接从1209开始读取。
然而在JavaScript中,数组并不是你想象中的那样连续的(continuous),因为它本质上属于一种特殊的对象,其实现类似哈希映射(hash-maps)或字典(dictionaries),如链表。所以,如果在JS中声明一个数组
const arr = new Array(4)
,计算机将生成类似下图的结构,如果程序需要读取arr[2],仍需要从1201开始遍历寻址。
如图
这就是JS 数组与真实数组的不同之处,显而易见,数学计算比遍历链表快,就长数组而言,情况尤其如此。
JS数组的进化
近几年来,JS的标准不断完善,性能也在不断提升。实际上,现代的JS引擎是会给数组分配连续内存的--如果数组是同质的(所有元素类型相同)。优秀的程序员总会保证数组同质,以便JIT(即时编译器)能够使用c编译器式的计算方式读取元素。
不过,一旦你想要在某个同质数组中插入一个其它类型的元素,JIT将解构整个数组,并按照旧有的方式重新创建。
因此,如果你的代码写的不太糟,JS Array对象在幕后仍然保持着真正的数组形式,这对现代JS开发者来说极为重要。
此外,数组跟随ES2015有了更多的演进,TC39决定引入类型化数组(Typed Arrays),于是我们就有了ArrayBuffer。
ArrayBuffer提供一块连续内存供我们随意操作。然而,直接操作内存还是太复杂、偏底层,于是便有了处理ArrayBuffer的视图(View)。目前已有一些可用视图,未来还会有更多加入。
var buffer = new ArrayBuffer(8);
var view = new Int32Array(buffer);
view[0] = 100;
高性能、高效率的类型化数组在WebGl之后被引入。WebGL工作者遇到了极大的性能问题,即如何高效处理二进制数据。另外,你也可以使用SharedArrayBuffer在多个Web Worker进程之间共享数据,以提升性能。
旧式数组 VS 类型化数组 :性能
前面已经讨论了JS数组的演进,现在来测试现代数组到底能给我们带来多大收益(环境:windows操作系统 node v8.1.3)
- 旧式数组:插入
const LIMIT = 10000000;
const arr = new Array(LIMIT);
console.time('Array insertion time');
for (let i = 0; i < LIMIT; i++) {
arr[i] = i;
}
console.timeEnd('Array insertion time');//26ms
- Typed Array:插入
const LIMIT = 10000000;
const buffer = new ArrayBuffer(LIMIT * 4);
const arr = new Int32Array(buffer);
console.time('Array insertion time');
for (let i = 0; i < LIMIT; i++) {
arr[i] = i;
}
console.timeEnd('Array insertion time');//30ms
旧式数组和ArrayBuffer的性能不相上下?NoNoNo,出现这种情况的原因是因为现代编译器已经智能化,能够将元素类型相同的传统数组在内部转换为内存连续的数组。尽管使用了new Array(LIMIT),数组实际依然以现代数组形式存在。
接着修改第一例子,将数组改成异构型(元素类型不完全一致)的,来看看是否存在性能差异。
- 旧式数组:插入
const LIMIT = 10000000;
const arr = new Array(LIMIT);
arr.push({a:1})
console.time('Array insertion time');
for (let i = 0; i < LIMIT; i++) {
arr[i] = i;
}
console.timeEnd('Array insertion time');//756ms
改变发生在第三行,将数组变为异构类型,其余代码保持不变,性能差异表现出来了,慢了29倍。
- 旧式数组:读取
const LIMIT = 10000000;
const arr = new Array(LIMIT);
arr.push({a:1})
for (let i = 0; i < LIMIT; i++) {
arr[i] = i;
}
let p;
console.time('Array read time');
for(let i=0;i<LIMIT;i++){
p=arr[i];
}
console.timeEnd('Array read time');//116ms
- Typed Array:读取
const LIMIT = 10000000;
const buffer = new ArrayBuffer(LIMIT * 4);
const arr = new Int32Array(buffer);
for (let i = 0; i < LIMIT; i++) {
arr[i] = i;
}
let p;
console.time('Array read time');
for(let i=0;i<LIMIT;i++){
p=arr[i];
}
console.timeEnd('Array read time');//119ms
此处的测试应该是不够准确,我发现在上述的所有例子中,当把let替换为var时,耗时显著减少,这里应该是创建块级作用域耗费了性能,似乎无法证明Typed Array的性能。
虽然测试没有获得可信的数据,但类型化数组的引入是有显著意义的,Int8Array,Uint8Array,Uint8ClampedArray,
Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,这些是类型化数组视图,使用原生字节序(与本机相同),还可以使用Data View 创建自定义视图窗口。未来应该会有更多帮助我们轻松操作ArrayBuffer的Data View库。JS数组的演进非常棒,现在它们速度快、效率高、健壮,在内存分配时也足够智能。
网友评论