首頁>技術>

引言

標註是地圖最基本的元素之一,標明了地圖每個位置或線路的名稱。在地圖 JSAPI 中,標註的展示效果及效能也是需要重點解決的問題。

新版地圖標註的設計中,引入了 SDF ( signed distance field)重構了整個標註部分的程式碼。新的方式需要把標註的位置偏移,避讓,三角拆分等全部由前端進行計算,不僅計算量激增,記憶體的消耗也成了重點關注的問題之一。

例如,3D 場景下需要構建大量的頂點座標,一萬左右的帶文字的標註,資料量大約會達到 8 (attributes) 5 (1個圖示 + 4個字) 6(個頂點) 1E4 ,約為 250w 個頂點,使用 Float32Array 儲存,需要的空間約為 2.5E6 4(byte)空間(海量地圖標註 DEMO)。前端這樣大量的儲存消耗,需要對記憶體的使用十分小心謹慎。於是藉此機會研究了一下前端記憶體相關的問題,以便在開發過程中做出更優的選擇,減少記憶體消耗,提高程式效能。

01 前端記憶體使用概述

首先我們來了解一下記憶體的結構。

記憶體結構

記憶體分為堆(heap)和棧(stack),堆記憶體儲存複雜的資料型別,棧記憶體則儲存簡單資料型別,方便快速寫入和讀取資料。在訪問資料時,先從棧內尋找相應資料的儲存地址,再根據獲得的地址,找到堆內該變數真正儲存的內容讀取出來。

在前端中,被儲存在棧內的資料包括小數值型,string ,boolean 和複雜型別的地址索引。

所謂小數值資料(small number), 即長度短於 32 位儲存空間的 number 型資料。

一些複雜的資料型別,諸如 Array,Object 等,是被存在堆中的。如果我們要獲取一個已儲存的物件 A,會先從棧中找到這個變數儲存的地址,再根據該地址找到堆中相應的資料。如圖:

簡單的資料型別由於儲存在棧中,讀取寫入速度相對複雜型別(存在堆中)會更快些。下面的 Demo 對比了存在堆中和棧中的寫入效能:

實驗環境1:mac OS/firefox v66.0.2對比結果:

實驗環境2:

mac OS/safari v11.1(13605.1.33.1.2)

對比結果:

在每個函式執行 10w 次的資料量下,可以看出在棧中的寫入操作是快於堆的。

物件及陣列的儲存

在JS中,一個物件可以任意新增和移除屬性,似乎沒有限制(實際上需要不能大於 2^32 個屬性)。而JS中的陣列,不僅是變長的,可以隨意新增刪除陣列元素,每個元素的資料型別也可以完全不一樣,更不一般的是,這個陣列還可以像普通的物件一樣,在上面掛載任意屬性,這都是為什麼呢?

Object 儲存

首先了解一下,JS是如何儲存一個物件的。

連結串列結構,移除和插入的效率非常高,但是讀取效率過低,也不可取;

複雜一些的樹結構等等,雖然不同的樹結構有不同的優點,但都繞不過建樹時較複雜,導致初始化效率低下;

雜湊表

雜湊表儲存是一種常見的資料結構。所謂雜湊對映,是把任意長度的輸入通過雜湊演算法變換成固定長度的輸出。

對於一個 JS 物件,每一個屬性,都按照一定的雜湊對映規則,對映到不同的儲存地址上。在我們尋找該屬性時,也是通過這個對映方式,找到儲存位置。當然,這個對映演算法一定不能過於複雜,這會使對映效率低下;但也不能太簡單,過於簡單的對映方式,會導致無法將變數均勻的對映到一片連續的儲存空間內,而造成頻繁的雜湊碰撞。

關於雜湊的對映演算法有很多著名的解決方案,此處不再展開。

雜湊碰撞

所謂雜湊碰撞,指的是在經過雜湊對映計算後,被對映到了相同的地址,這樣就形成了雜湊碰撞。想要解決雜湊碰撞,則需要對同樣被對映過來的新變數進行處理。

眾所周知,JS 的物件是可變的,屬性可在任意時候(大部分情況下)新增和刪除。在最開始給一個物件分配記憶體時,如果不想出現雜湊碰撞問題,則需要分配巨大的連續儲存空間。但大部分的物件所包含的屬性一般都不會很長,這就導致了極大的空間浪費。

但是如果一開始分配的記憶體較少,隨著屬性數量的增加,必定會出現雜湊碰撞,那如何解決雜湊碰撞問題呢?

對於雜湊碰撞問題,比較經典的解決方法有如下幾種:

開放定址法再雜湊法拉鍊法

這幾種方式均各有優略,由於本文不是重點講述雜湊碰撞便不再綴餘。

在 JS 中,選擇的是拉鍊法解決雜湊碰撞。所謂拉鍊法,是將通過一定演算法得到的相同對映地址的值,用連結串列的形式儲存起來。如圖所示(以傾斜的箭頭表明連結串列動態分配,並非連續的記憶體空間):

對映後的地址空間儲存的是一個連結串列的指標,一個連結串列的每個單元,儲存著該屬性的 key, value 和下一個元素的指標;

這種儲存的方式的好處是,最開始不需要分配較大的儲存空間,新新增的屬性只要動態分配記憶體即可;

對於索引,新增和移除都有相對較好的效能;

通過上述介紹,也就解釋了這個小節最開始提出的為何JS 的物件如此靈活的疑問。

Array 儲存

JS 的陣列為何也比其他語言的陣列更加靈活呢?因為 JS 的 Array 的物件,就是一種特殊型別的陣列!

所謂特殊型別,就是指在 Array 中,每一個屬性的 key 就是這個屬性的 index;而這個物件還有 .length 屬性;還有 concat, slice, push, pop 等方法;

於是這就解釋了:

為何 JS 的陣列每個資料型別都可以不一樣?因為他就是個物件,每條資料都是一個新分配的型別連入連結串列中;為何 JS 的陣列無需提前設定長度,是可變陣列?答案同上;為何陣列可以像 Object 一樣掛載任意屬性?因為他就是個物件;

等等一系列的問題。

記憶體攻擊

當然,選擇任何一種資料儲存方式,都會有其不利的一面。這種雜湊的拉鍊演算法在極端情況下也會造成嚴重的記憶體消耗。

我們知道,良好的雜湊對映演算法,可以講資料均勻的對映到不同的地址。但如果我們掌握了這種對映規律而將不同的資料都對映到相同的地址所對應的連結串列中去,並且資料量足夠大,將造成記憶體的嚴重損耗。讀取和插入一條資料會中了連結串列的缺陷,從而變得異常的慢,最終拖垮記憶體。這就是我們所說的記憶體攻擊。

構造一個 JSON 物件,使該物件的 key 大量命中同一個地址指向的列表,附件為 JS 程式碼,只包含了一個特意構造的物件(引用出處),圖二為利用 Performance 檢視的效能截圖:

相同 size 物件的 Performance 對比圖:

根據 Performance 的截圖來看,僅僅是 load 一個 size 為 65535 的物件,竟然足足花費了 40 s!而相同大小的非共計資料的執行時間可忽略不計。

如果被使用者利用了這個漏洞,構建更長的 JSON 資料,可以直接把服務端的記憶體打滿,導致服務不可用。這些地方都需要開發者有意識的避免。

但從本文的來看,這個示例也很好的驗證了我們上面所說的物件的儲存形式。

02 檢視型別(連續記憶體)

通過上面的介紹與實驗可以知道,我們使用的陣列實際上是偽陣列。這種偽陣列給我們的操作帶來了極大的方便性,但這種實現方式也帶來了另一個問題,及無法達到陣列快速索引的極致,像文章開頭時所說的上百萬的資料量的情況下,每次新新增一條資料都需要動態分配記憶體空間,資料索引時都要遍歷連結串列索引造成的效能浪費會變得異常的明顯。

好在 ES6 中,JS 新提供了一種獲得真正陣列的方式:ArrayBuffer,TypedArray 和 DataView

ArrayBuffer

ArrayBuffer 代表分配的一段定長的連續記憶體塊。但是我們無法直接對該記憶體塊進行操作,只能通過 TypedArray 和 DataView 來對其操作。

TypedArray

TypeArray 是一個統稱,他包含 Int8Array / Int16Array / Int32Array / Float32Array等等。

拿 Int8Array 來舉例,這個物件可拆分為三個部分:Int、8、Array

首先這是一個數組,這個資料裡儲存的是有符號的整形資料,每條資料佔 8 個位元位,及該資料裡的每個元素可表示的最大數值是 2^7 = 128 , 最高位為符號位。

// DataViewvar arrayBuffer = new ArrayBuffer(8 * 10);

var dataView = new DataView(arrayBuffer);

dataView.setInt8(0, 2);dataView.setFloat32(8, 65535);

// 從偏移位置開始獲取不同資料dataView.getInt8(0);// 2dataView.getFloat32(8);// 65535

// 普通陣列function arrayFunc(){

}

// dataViewfunction dataViewFunc(){

}

// typedArrayfunction typedArrayFunc(){

}

// mainvar worker = new Worker('./worker.js');

worker.onmessage = function getMessageFromWorker(e){

};

var msg = [1, 2, 3];

worker.postMessage(msg);

// workeronmessage = function(e){

};

function increaseData(data){

}

var worker = new Worker('./sharedArrayBufferWorker.js');

worker.onmessage = function(e){// 傳回到主執行緒已經被計算過的資料

// 和傳統的 postMessage 方式對比,發現主執行緒的原始資料發生了改變

};

var sharedArrayBuffer = new SharedArrayBuffer(3);var int8Array = new Int8Array(sharedArrayBuffer);

int8Array[0] = 1;int8Array[1] = 2;int8Array[2] = 3;

worker.postMessage(sharedArrayBuffer);

onmessage = function(e){

};

function increaseData(arrayData){

}

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 震撼!用Python開發網站如此簡單