JavaScript的稀疏數組和密集數組

文章首發:github.com/qiudongwei/…git

測試一段代碼

運行環境:Chrome JavaScript | V8 8.9.255.20github

let array1 = [1];
let array2 = [1];
// array2[2]=2; // 超出數組長度

console.time('array1');  
for (let i = 0; i < 10000000; i++) {  
    array1.push(i); 
}  
console.timeEnd('array1'); 


console.time('array2');  
for (let j = 0; j < 10000000; j++) {  
    array2.push(j); 
}  
console.timeEnd('array2'); 
複製代碼

① 測試幾回發現,array1和array2耗時相近,幾乎是同樣的; ② 取消第三行的註釋再運行,測試發現,array1的耗時明顯要小於array2;數組

或者直接在這測試:jsben緩存

這是爲何呢?V8內部是如何處理數組操做的?性能優化

數組中的元素類型

在JavaScript層面,開發者能夠給數組的元素設置任意基礎數據類型,eg:arr = [1, 1.2, 'hello', {}, []],然而,在V8層面,它理解的元素類型卻只有三種:markdown

  • SMI_ELEMENTS:又稱Smi,小整數(Small integers)
  • DOUBLE_ELEMENTS:雙精度浮點數,浮點數和不能表示爲 Smi 的整數
  • ELEMENTS:常規元素,不能表示爲 Smi 或雙精度的值

舉個例子,暫時忽略PACKED_這個前綴:app

const arr = [1, 2, 3]; // 元素類型: PACKED_SMI_ELEMENTS
arr.push(1.54);         // 元素類型變爲: PACKED_DOUBLE_ELEMENTS
arr.push('c');            // 元素類型變爲: PACKED_ELEMENTS
複製代碼

由上例子可知,V8爲每一個數組分配一種元素類型 SMI_ELEMENTS | DOUBLE_ELEMENTS | ELEMENTS,而且元素類型的轉換隻能從特定的(eg:SMI_ELEMENTS )到更通常的(eg:ELEMENTS)。函數

再看一個例子:oop

const arr = [1, 2, ,4]; // 元素類型HOLEY_SMI_ELEMENTS
arr.push(5);               // 元素類型變爲: HOLEY_DOUBLE_ELEMENTS
arr.push('a');             // 元素類型變爲: HOLEY_ELEMENTS
複製代碼

這個例子裏面,arr出現了個洞(沒有被完滿填充)咱們稱其爲稀疏數組,用一個HOLEY去標識其類型; PACKED則表示這是個密集數組。 稀疏數組是不可逆的,一旦標記爲有空,它就是永遠有洞,即使它被填充了!稀疏數組不可轉變爲密集數組,反之卻能夠。以下圖代表了各元素類型之間的轉換關係: image性能

而數組的稀疏與否,有可能(數據量足夠大的時候)會影響數組操做的性能。

數組的性能問題

  1. 對於稀疏數組,V8須要對其原型鏈進行額外的檢查和昂貴的查找;
  2. 通常來講,更具體的元素種類能夠進行更細粒度的優化。元素類型越通常,對該對象的操做就會越慢;
  3. V3依賴其內嵌緩存(inline cache)機制能夠更有效地去對具體類型的(密集)數組做數組操做的優化;

避免建立稀疏數組

  1. 避免new Array建立數組
// bad
const arr = new array(5);

// good
const arr = [0, 0, 0, 0, 0];

// better
const arr = Array.from({length: 5}
複製代碼
  1. 避免越界訪問數組
const arr = [1, 2, 3];

// bad
log(arr[3]);
複製代碼
  1. 避免給length賦值
const arr = [1, 2, 3];

// bad
arr.length = 5;
複製代碼

數組多態

若是您的代碼須要處理包含多種不一樣元素類型的數組,則可能會比單個元素類型數組要慢,由於你的代碼要對不一樣類型的數組元素進行多態操做。看這個例子:

const each = (array, callback) => {
  for (let index = 0; index < array.length; ++index) {
    const item = array[index];
    callback(item);
  }
};

// 第一次調用
each(['a', 'b', 'c'], doSomething);

// 第二次調用
each([1.1, 2.2, 3.3], doSomething);

// 第三次調用
each([1, 2, 3], doSomething);
複製代碼

在第一次調用中,數組類型是 PACKED_ELEMENTS,V8使用 inline cache(內嵌緩存)記住 each 函數是被一個特定元素類型調用的,在下一次調用時,V8會先檢查數組類型是不是 PACKED_ELEMENTS,若是是,則複用上次生成的代碼,不然會作更多的事情,如從新生成相應的代碼;

第二次調用的時候,數組類型是 PACKED_DOUBLE_ELEMENTS,V8檢查到出現了與緩存中不同的數組類型,此時,V8將 each 函數中的數組標記爲多態,接下來的每一次 each 調用都須要進行 PACKED_ELEMENTSPACKED_DOUBLE_ELEMENTS 兩種類型的檢查,這會形成一些性能消耗;

第三次調用結束的時候,V8緩存中出現了3種不一樣類型的 each 緩存,V8爲了可以複用生成的代碼,接下來的每一次 each 調用,都會作相應的類型檢查。

然而,內置方法(eg:Array.prototype.forEach)能夠更有效地處理這種多態性,所以在性能敏感的狀況下應用優先考慮使用它。

參考:

V8 中的元素種類及性能優化

相關文章
相關標籤/搜索