为何要作这个对比呢?由于昨天看到了这篇文章《如何使用 Set 来提升代码的性能》,文章中说到的好多东西我并不认同,原做者一味的推崇Set比数组要快,可是在我认知中彷佛没有别的数据结构在存取方面能比线性存储的数组更快。而原文下面的一些测试只用一组数据对比,这让我感受原做者根本就没有认真对比。因此萌生了本身测试一把的想法。javascript
原本题目打算取成《驳:<如何使用 Set 来提升代码的性能>》的😂,测试完发现并无什么好驳的,就不给前端娱乐圈添加新闻了。前端
废话很少说,下面放上测试数据java
为了可以清楚的看到内存的消耗,测试在浏览器和Node中分别跑一次,以Node中的内存消耗为参考。数据量为1e7git
const MB_SIZE = 1024 * 1024;
function formatMemory (size) {
return size < MB_SIZE ? `${(size/1024).toFixed(3)}KB` : `${(size/MB_SIZE).toFixed(3)}MB`
}
function getMemory() {
let memory = process.memoryUsage()
console.log('------------------------------------')
console.log('总占用:', formatMemory(memory.rss))
console.log('堆内存:', formatMemory(memory.heapTotal))
console.log('堆内存(使用中):', formatMemory(memory.heapUsed))
console.log('V8占用:', formatMemory(memory.external))
console.log('------------------------------------')
}
module.exports = getMemory
复制代码
const COUNT = 1e7
console.time('new_arr')
let arr = []
console.timeEnd('new_arr')
getMemory()
// 建立时间
console.time('push_arr')
for(let i = 0; i < COUNT; i++) {
arr.push(i)
}
console.timeEnd('push_arr')
// 赋值时间
console.time('push_arr2')
for(let i = 0; i < COUNT; i++) {
arr[i] = i + 1
}
console.timeEnd('push_arr2')
// 赋值时间
console.time('push_arr3')
for(let i = 0; i < COUNT; i++) {
arr[i] = i - 1
}
console.timeEnd('push_arr3')
// 单循环时间
console.time('for_time')
for(let i = 0; i < COUNT; i++) {
// arr[i] = i
}
console.timeEnd('for_time')
getMemory()
复制代码
测试结果为:github
new_arr: 0.156ms
------------------------------------
总占用: 20.820MB
堆内存: 9.234MB
堆内存(使用中): 4.060MB
V8占用: 85.453KB
------------------------------------
push_arr: 221.450ms
push_arr2: 10.103ms
push_arr3: 9.332ms
for_time: 6.687ms
------------------------------------
总占用: 362.148MB
堆内存: 350.246MB
堆内存(使用中): 344.085MB
V8占用: 8.477KB
------------------------------------
复制代码
能够看到因为js中的Array
是动态建立的因此第一次push建立数组的时候相对与后面的直接存取时间相差了20倍左右,而且直接存取的大部分时间仍是循环所耗费的。可见动态建立申请内存很是的耗费时间,内存申请到后的存取就很是符合O(1)的时间了,下面看一下Set
的表现。json
const COUNT = 1e7
console.time('new Set')
let set = new Set()
console.timeEnd('new Set')
getMemory()
console.time('set_add')
for(let i = 0; i < COUNT; i++) {
set.add(i)
}
console.timeEnd('set_add')
getMemory()
复制代码
测试结果为:数组
new Set: 0.169ms
------------------------------------
总占用: 20.828MB
堆内存: 9.234MB
堆内存(使用中): 4.057MB
V8占用: 85.453KB
------------------------------------
set_add: 2282.407ms
------------------------------------
总占用: 662.043MB
堆内存: 650.227MB
堆内存(使用中): 644.690MB
V8占用: 85.453KB
------------------------------------
复制代码
能够看到一样的数量Set
建立速度比Array
要慢了近10倍,内存占用也多出了近2倍。咱们都知道Set
底层有红黑树
和HashSet
两种实现方式,都是以空间换时间。虽然没有看过V8
的具体实现,可是因为JS中Set
是无序的,结合后面测试set.has()
是O(1)的时间复杂度,能够猜想V8
中的Set
是以Hash
的形式实现的,有Hash
必然涉及到动态扩张Hash
或者链式Hash
,空间天然会占用的多。浏览器
首先咱们经过下面这段代码随机生成10000个元素来进行查找的测试性能优化
const fs = require('fs')
function random () {
return ~~(Math.random() * 1e7)
}
let arr = []
for(let i = 0; i < 1e3; i++) {
arr.push(random())
}
fs.writeFileSync('./data.json', JSON.stringify(arr))
复制代码
console.time('indexOf_time')
for(let i = data.length; i >=0 ; i--) {
arr.indexOf(data[i])
}
console.timeEnd('indexOf_time')
console.time('includes_time')
for(let i = data.length; i >=0 ; i--) {
arr.includes(data[i])
}
console.timeEnd('includes_time')
复制代码
测试结果:数据结构
indexOf_time: 52451.386ms
includes_time: 52599.605ms
复制代码
console.time('set_has_time')
for(let i = data.length; i >=0 ; i--) {
set.has(data[i])
}
console.timeEnd('set_has_time')
复制代码
测试结果为:
set_has_time: 3.402ms
复制代码
能够看到对比很是夸张数组查询的速度基本比Set
慢了15000倍!,远超原文的7.54倍。这是为何呢?通过前面的分析咱们知道Set
底层又Hash
实现,查询的复杂度基本为O(1),查询时间并不会随着数据的增大而增大,而数组的查询为线性的O(n),因为咱们本次测试的数据量又达到了1e7的数量,因此查询的速度就相差了很是多了。
删除必然依赖查询,从查询的结果上来看咱们已经能够预见删除测试的结果了,因为Array
做为一个线性的数据结构是不存在删除操做的,通常来讲都是将某个位置置空来表示删除的,若是非要使用splice(index, 1)
来进行删除,那么至关与将index
后面全部的元素都移动了一次,至关于又是一次O(n)的操做,能够碰见性能必定好不了。为了节省时间此次咱们将删除的数据调整为1000个
建立删除辅助函数:
function deleteFromArr (arr, item) {
let index = arr.indexOf(item);
return index !== -1 && arr.splice(index, 1);
}
复制代码
function deleteFromArr (arr, item) {
let index = arr.indexOf(item);
return index !== -1 && arr.splice(index, 1);
}
console.time('includes_time')
for(let i = data.length; i >=0 ; i--) {
deleteFromArr(arr, data[i])
}
console.timeEnd('includes_time')
复制代码
测试结果为:
deleteFromArr_time: 8245.150ms
复制代码
console.time('set_delete_time')
for(let i = data.length; i >=0 ; i--) {
set.delete(data[i])
}
console.timeEnd('set_delete_time')
复制代码
测试结果为:
set_delete_time: 0.574ms
复制代码
显然结果与咱们预测的同样,一样Set
的性能远超Array
内存(MB) | 建立时间(MS) | 查询(MS) | 删除(MS) | |
---|---|---|---|---|
Array | 344 | 221 | 5245 | 8245 |
Set | 644 | 2282 | 0.34 | 0.574 |
Array
占优点以外其余两种状况都是
Set
远超
Array
,可是别忘了咱们上面的数据量是多少?
1e7! 这是一个平时代码中几乎接触不到的数据量,哪怕是在Node里我也想象不到什么场景下须要咱们手动在内存里操做一个1e7数量级的
Array
,更别说浏览器的环境下了。通常在客户端下咱们的操做的数据不会超过
1e3
,单个页面上千的数据映射到DOM上已经很是卡了,更别说更高的数量级了。下面咱们看一下
1e3
级别的性能对比,能够看到所有都是
1MS
都不到的数据,小数据下使用时根本不用考虑二者性能的开支,你多操做一次DOM形成的性能开始都比你操做数据的开支要多的多了。因此平常开发中
在适合的场景使用适合的工具,有时你考虑的性能在整个环节中根本就是微不足道的
内存(MB) | 建立时间(MS) | 查询(MS) | 删除(MS) | |
---|---|---|---|---|
Array | 4 | 0.61 | 0.14 | 0.135 |
Set | 4 | 0.81 | 0.102 | 0.08 |
以上,并非说性能不重要,而是要在合适的场景去考虑合适的性能优化,在客户端考虑一下如何减小重排重绘要比考虑使用Array仍是Set能减小更多的开销,在服务端合理的设计可能会比考虑这个带来更大的优化。
最后,测试的全部代码都在这里github.com/xluos/Compa…