深入 JavaScript 数组:进化与性能

图片 4

深入 JavaScript 数组:进化与性能

2017/09/18 · JavaScript
· 数组

原文出处: Paul
Shan   译文出处:众成翻译   

正式开始前需要声明,本文并不是要讲解 JavaScript
数组基础知识,也不会涉及语法和使用案例。本文讲得更多的是内存、优化、语法差异、性能、近来的演进。

在使用 JavaScript 前,我对 C、C++、C# 这些已经颇为熟悉。与许多 C/C++
开发者一样,JavaScript 给我的第一印象并不好。

Array 是主要原因之一。JavaScript
数组不是连续(contiguous)的,其实现类似哈希映射(hash-maps)或字典(dictionaries)。我觉得这有点像是一门
B 级语言,数组实现根本不恰当。自那以后,JavaScript
和我对它的理解都发生了变化,很多变化。

/* 原文阅读自前端早读课1071期 */

为什么说 JavaScript 数组不是真正的数组

在聊 JavaScript 之前,先讲讲 Array 是什么。

数组是一串连续的内存位置,用来保存某些值。注意重点,“连续”(continuous,或
contiguous),这很重要。

图片 1

上图展示了数组在内存中存储方式。这个数组保存了 4 个元素,每个元素 4
字节。加起来总共占用了 16 字节的内存区。

假设我们声明了 tinyInt arr[4];,分配到的内存区的地址从 1201
开始。一旦需要读取 arr[2],只需要通过数学计算拿到 arr[2]
的地址即可。计算 1201 + (2 X 4),直接从 1209 开始读取即可。

图片 2

JavaScript
中的数据是哈希映射,可以使用不同的数据结构来实现,如链表。所以,如果在
JavaScript 中声明一个数组
var arr = new Array(4),计算机将生成类似上图的结构。如果程序需要读取
arr[2],则需要从 1201 开始遍历寻址。

以上急速 JavaScript
数组与真实数组的不同之处。显而易见,数学计算比遍历链表快。就长数组而言,情况尤其如此。

为什么说JavaScript数组不是真正的数组

  数组是用来存储元素的线性集合,在内存中占据一串连续的内存位置。注意重点,“连续”(continuous)。

图片 3

如图

上图展示了数组在内存中的存储方式,这个数组保存了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开始遍历寻址。

图片 4

如图

这就是JS
数组与真实数组的不同之处,显而易见,数学计算比遍历链表快,就长数组而言,情况尤其如此。

JavaScript 数组的进化

不知你是否记得我们对朋友入手的 256MB
内存的电脑羡慕得要死的日子?而今天,8GB 内存遍地都是。

与此类似,JavaScript 这门语言也进化了不少。从 V8、SpiderMonkey 到 TC39
和与日俱增的 Web 用户,巨大的努力已经使 JavaScript
成为世界级必需品。一旦有了庞大的用户基础,性能提升自然是硬需求。

实际上,现代 JavaScript 引擎是会给数组分配连续内存的 ——
如果数组是同质的(所有元素类型相同)。优秀的程序员总会保证数组同质,以便
JIT(即时编译器)能够使用 c 编译器式的计算方法读取元素。

不过,一旦你想要在某个同质数组中插入一个其他类型的元素,JIT
将解构整个数组,并按照旧有的方式重新创建。

因此,如果你的代码写得不太糟,JavaScript Array
对象在幕后依然保持着真正的数组形式,这对现代 JS 开发者来说极为重要。

此外,数组跟随 ES2015/ES6 有了更多的演进。TC39 决定引入类型化数组(Typed
Arrays),于是我们就有了 ArrayBuffer

ArrayBuffer
提供一块连续内存供我们随意操作。然而,直接操作内存还是太复杂、偏底层。于是便有了处理
ArrayBuffer 的视图(View)。目前已有一些可用视图,未来还会有更多加入。

var buffer = new ArrayBuffer(8); var view = new Int32Array(buffer);
view[0] = 100;

1
2
3
var buffer = new ArrayBuffer(8);
var view   = new Int32Array(buffer);
view[0] = 100;

了解更多关于类型化数组(Typed Arrays)的知识,请访问 MDN
文档。

高性能、高效率的类型化数组在 WebGL 之后被引入。WebGL
工作者遇到了极大的性能问题,即如何高效处理二进制数据。另外,你也可以使用
SharedArrayBuffer
在多个 Web Worker 进程之间共享数据,以提升性能。

从简单的哈希映射到现在的 SharedArrayBuffer,这相当棒吧?

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 类型化数组:性能

前面已经讨论了 JavaScript
数组的演进,现在来测试现代数组到底能给我们带来多大收益。下面是我在 Mac
上使用 Node.js 8.4.0 进行的一些微型测试结果。

旧式数组 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数组的演进非常棒,现在它们速度快、效率高、健壮,在内存分配时也足够智能。

旧式数组:插入

var LIMIT = 10000000; var arr = new Array(LIMIT); console.time(“Array
insertion time”); for (var i = 0; i< LIMIT; i++) { arr[i] = i; }
console.timeEnd(“Array insertion time”);

1
2
3
4
5
6
7
var LIMIT = 10000000;
var arr = new Array(LIMIT);
console.time("Array insertion time");
for (var i = 0; i< LIMIT; i++) {
arr[i] = i;
}
console.timeEnd("Array insertion time");

用时:55ms

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图