JavaScript 数组的进化与性能分析
正式开始前需要声明,本文并不是要讲解 javascript 数组基础知识,也不会涉及语法和使用案例。本文讲得更多的是内存、优化、语法差异、性能、近来的演进。
在使用 javascript 前,我对 c、c++、c# 这些已经颇为熟悉。与许多 c/c++ 开发者一样,javascript 给我的第一印象并不好。
array 是主要原因之一。javascript 数组不是连续(contiguous)的,其实现类似哈希映射(hash-maps)或字典(dictionaries)。我觉得这有点像是一门 b 级语言,数组实现根本不恰当。自那以后,javascript 和我对它的理解都发生了变化,很多变化。
为什么说 javascript 数组不是真正的数组
在聊 javascript 之前,先讲讲 array 是什么。
数组是一串连续的内存位置,用来保存某些值。注意重点,“连续”(continuous,或 contiguous),这很重要。
上图展示了数组在内存中存储方式。这个数组保存了 4 个元素,每个元素 4 字节。加起来总共占用了 16 字节的内存区。
假设我们声明了 tinyint arr[4];,分配到的内存区的地址从 1201 开始。一旦需要读取 arr[2],只需要通过数学计算拿到 arr[2] 的地址即可。计算 1201 + (2 x 4),直接从 1209 开始读取即可。
javascript 中的数据是哈希映射,可以使用不同的数据结构来实现,如链表。所以,如果在 javascript 中声明一个数组 var arr = new array(4),计算机将生成类似上图的结构。如果程序需要读取 arr[2],则需要从 1201 开始遍历寻址。
以上急速 javascript 数组与真实数组的不同之处。显而易见,数学计算比遍历链表快。就长数组而言,情况尤其如此。
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;
了解更多关于类型化数组(typed arrays)的知识,请访问 mdn 文档。
高性能、高效率的类型化数组在 webgl 之后被引入。webgl 工作者遇到了极大的性能问题,即如何高效处理二进制数据。另外,你也可以使用 sharedarraybuffer 在多个 web worker 进程之间共享数据,以提升性能。
从简单的哈希映射到现在的 sharedarraybuffer,这相当棒吧?
旧式数组 vs 类型化数组:性能
前面已经讨论了 javascript 数组的演进,现在来测试现代数组到底能给我们带来多大收益。下面是我在 mac 上使用 node.js 8.4.0 进行的一些微型测试结果。
旧式数组:插入
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
typed array:插入 var limit = 10000000; var buffer = new arraybuffer(limit * 4); var arr = new int32array(buffer); console.time("arraybuffer insertion time"); for (var i = 0; i < limit; i++) { arr[i] = i; } console.timeend("arraybuffer insertion time");
用时:52ms
擦,我看到了什么?旧式数组和 arraybuffer 的性能不相上下?不不不。请记住,前面提到过,现代编译器已经智能化,能够将元素类型相同的传统数组在内部转换成内存连续的数组。第一个例子正是如此。尽管使用了 new array(limit),数组实际依然以现代数组形式存在。
接着修改第一例子,将数组改成异构型(元素类型不完全一致)的,来看看是否存在性能差异。
旧式数组:插入(异构) var limit = 10000000; var arr = new array(limit); arr.push({a: 22}); console.time("array insertion time"); for (var i = 0; i < limit; i++) { arr[i] = i; } console.timeend("array insertion time");
用时:1207ms
改变发生在第 3 行,添加一条语句,将数组变为异构类型。其余代码保持不变。性能差异表现出来了,慢了 22 倍。
旧式数组:读取
var limit = 10000000; var arr = new array(limit); arr.push({a: 22}); for (var i = 0; i < limit; i++) { arr[i] = i; } var p; console.time("array read time"); for (var i = 0; i < limit; i++) { //arr[i] = i; p = arr[i]; } console.timeend("array read time");
用时:196ms
typed array:读取 var limit = 10000000; var buffer = new arraybuffer(limit * 4); var arr = new int32array(buffer); console.time("arraybuffer insertion time"); for (var i = 0; i < limit; i++) { arr[i] = i; } console.time("arraybuffer read time"); for (var i = 0; i < limit; i++) { var p = arr[i]; } console.timeend("arraybuffer read time");
用时:27ms
结论
类型化数组的引入是 javascript 发展历程中的一大步。int8array,uint8array,uint8clampedarray,int16array,uint16array,int32array,uint32array,float32array,float64array,这些是类型化数组视图,使用原生字节序(与本机相同)。我们还可以使用 dataview 创建自定义视图窗口。希望未来会有更多帮助我们轻松操作 arraybuffer 的 dataview 库。
javascript 数组的演进非常 nice。现在它们速度快、效率高、健壮,在内存分配时也足够智能。
总结
以上所述是小编给大家介绍的javascript 数组的进化与性能分析,希望对大家有所帮助
上一篇: JS闭包的几种常见形式实例详解
推荐阅读
-
PHP合并数组+与array_merge的区别分析
-
JSON基本语法及与JavaScript的异同实例分析
-
javascript数组与php数组的地址传递及值传递用法实例
-
JavaScript去掉数组重复项的方法分析【测试可用】
-
JavaScript遍历数组的三种方法map、forEach与filter实例详解
-
JavaScript实现浅拷贝与深拷贝的方法分析
-
Oracle性能优化——SQL基线(SQLbaseline)的载入与进化(11g中引入的基线)
-
JS常用的几种数组遍历方式以及性能分析对比实例详解
-
php中用加号与用array_merge合并数组的区别深入分析
-
php中serialize序列化与json性能测试的示例分析