咱們是否應該使用 Set 來提升代碼的性能

前言

爲何要作這個對比呢?由於昨天看到了這篇文章《如何使用 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
複製代碼

建立&添加元素對比

Array

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

Set

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))
複製代碼

Array

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
複製代碼

Set

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);
}
複製代碼

Array

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
複製代碼

Set

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…

相關文章
相關標籤/搜索