深究 JavaScript 數組 —— 演進&性能

作者:Paul Shan 原文:Diving deep into JavaScript array - evolution & performance

寫文章前我要說一下,這篇文章不是講 JavaScript 數組基礎的,也不會教相關的語法和用法。文章更多的是講數組在內存中的存儲方式、優化、不同語法導致的行爲差異、性能和最近的改進。

我接觸 JavaScript 的時候,已經對 C/C++/C# 等語言相當熟悉了。但和許多 C/C++使用者一樣,第一次與 JavaScipt 的「約會」並不愉快。

我不喜歡 JavaScipt 的一個主要原因是它的Array。 JavaScript 的數組通過哈希映射或者字典的方式來實現,所以不是連續的。我覺得這是一門劣等語言:連數組都不能正確的實現。但時至今日, JavaScript 以及我對 JavaScript 的理解都有相當大的變化。

爲什麼 JavaScript 數組不是真正的數組

在講 JavaScript 相關的東西前,讓我先告訴你什麼是 Array。

數組( Array )在內存中用一串連續的區域來存放一些值。注意「連續」一詞,它至關重要。

上圖表示存儲在內存中的一個數組。存儲4個4 bit 的元素,一共需要16 bit 存儲區域,每個元素順序保持一致。

假設,我聲明瞭tinyInt arr[4],它佔用從1201位置開始的一溜存儲區域。當某個時刻我嘗試讀取a[2],那麼只要做簡單的計算,找到a[2]的位置就可以了。例如1201+(2X4)然後直接從1209位置讀取數據。

在 JavaScript 中,數組是哈希映射。它可以通過多種數據結構實現,其中一種是鏈表。所以,如果在 JavaScript 中聲明var arr = new Array(4);它會產生類似上圖的結構。因此,如果你想在程序中某一處讀取a[2],它必須從1201位置開始溯尋a[2]的位置。

這就是 JavaScript 數組和真正的數組不同的地方。顯然數學計算要比鏈表遍歷花的時間少。遇到長點的數組,日子就不好過了啊。

JavaScript 數組演進

還記得以前如果某個朋友的電腦有 256MB RAM我們多嫉妒嗎?但現在,8GB RAM 已經稀鬆平常了。

跟它一樣,JavaScript 這門語言也進化了許多。由於V8 、SpiderMonkey、TC39以及日益增多的 web 用戶的努力,世界已經離不開 JavaScript 了。擁有如此巨大的用戶羣體,性能提升也勢在必行。

近些日子, JavaScript 引擎已經在爲同種數據類型的數組分配連續的存儲空間了。優秀的開發者總是保持數組的數據類型一致,這樣即時編譯器 (JIT) 就能像 C 編譯器一樣通過計算讀取數組了。

但是,如果你想在同種類型的數組中插入不同類型的元素,JIT 會銷燬整個數組然後用以前的辦法重建。

所以,如果你沒寫垃圾代碼的話,JavaScript 的Array對象會維護一個真正的數組,這對現代 JS 開發者來說是一件大好事。

另外,在 ES2015/ES6 中, 數組還有其它改進。 TC39 決定在 JavaScript 中引入類型化數組,所以如今我們有 ArrayBuffer了。

ArrayBuffer 會有一大塊連續的存儲位置,你能用它做任何你想做的事情。不過,直接處理內存涉及非常底層的操作,相當複雜。

所以我們有 Views 來處理 ArrayBuffer。已經有一些可用的 View 了,未來還會加:

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

如果你想知道更多有關類型化數組的信息,可以去看看MDN 文檔

類型化數組性能良好且非常高效。WebGL 開發者因爲缺少高效處理二進制數據的手段而經常面臨性能問題,所以提出了類型化數組。你還可以使用SharedArrayBuffer在多個 web-workers 間共享內存數據來提升性能。

驚訝嗎?從簡單的哈希映射開始,我們現在已經在討論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

類型化數組-插入

var LIMIT = 10000000;
var buffer = new ArrayBuffer(LIMIT * 4);
var arr = new Int32Array(buffer);
console.time("ArrayBuffer insertion time");
for (var i = 0; i &lt; LIMIT; i++) {
    arr[i] = i;
}
console.timeEnd("ArrayBuffer insertion time");

所需時間:52ms

我擦,我看到了啥?舊數組和 ArraryBuffer 的性能一樣?不。回顧一下,前面我已經說過了,如今編譯器已經在爲類型一致的數組分配連續的存儲空間了。所以在第一個例子中,即使我用的是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

我在上面第三行插入了一個表達式讓數組的類型不一致,其它所有的都跟前面一模一樣。但是性能上有了巨大的變化:慢了整整22倍。

舊數組-讀取

var arr = new Array(LIMIT);
arr.push({a: 22});
for (var i = 0; i &lt; LIMIT; i++) {
    arr[i] = i;
}
var p;
console.time("Array read time");
for (var i = 0; i &lt; LIMIT; i++) {
    //arr[i] = i;
    p = arr[i];
}
console.timeEnd("Array read time");

所需時間:196ms

類型化數組-讀取

var LIMIT = 10000000;
var buffer = new ArrayBuffer(LIMIT * 4);
var arr = new Int32Array(buffer);
console.time("ArrayBuffer insertion time");
for (var i = 0; i &lt; LIMIT; i++) {
    arr[i] = i;
}
console.time("ArrayBuffer read time");
for (var i = 0; i &lt; LIMIT; i++) {
    var p = arr[i];
}
console.timeEnd("ArrayBuffer read time");

所需時間:27ms

結論

在 JavaScript 中引入類型化數組是一個巨大的進步,Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array 等都是類型化數組 view,按照原生的 byte 數排序。你也可以看看 DataView 創建自己的 view 窗口。希望在將來會有更多 DataView 庫方便我們使用 ArrayBuffer。

JavaScript 對數組做的改進很棒。現在它們快速、高效並且在分配內存時足夠聰明瞭。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章