本文受深刻探究Immutable.js的實現機制這篇文章啓發,結合本身對Map源碼的解讀,談談我對immutable-js中map數據結構的理解,如有不正確的地方,歡迎指正。數組
Trie 字典樹,一種用空間換取時間的樹形數據結構,主要特色是利用字符串的公共前綴來挺升查詢性能。好比一組字符串 ["abc","ab","bd","dda"] 它的字典樹結構以下:
數據結構
紅色節點表明按查找路徑下來能夠組成一個單詞,這樣在查找是否存在"abc"時,每一個字符逐個進行對比,時間複雜度爲O(len) = 3,len爲要查找的字符串的長度,而按照通常逐個對比的方式,時間複雜度爲O(lenxn) = 4x3 = 12,n爲字符串的個數,顯然字典樹的方式效率更高。app
Vector Trie 是在 Trie 的基礎上實現了以向量數組的形式進行數據分組存儲,每個被存儲的值所對應的key都映射爲數組的下標。
好比這樣的數據結構 {‘000’: ‘banana’, ‘001’: ‘grape’, ‘010’: ‘lemon’, ‘011’: ‘orange’, ‘100’: ‘apple’},每一個key映射爲數組的索引值,這樣生成簡單的兩位數組的Vector Trie是這樣子的:
post
咱們能夠將key映射爲數字,而後再對應到數組中,好比一個key爲 8899 映射爲 5 個長度(即5進制)的數字是241044, 那麼他要生成深度爲6的數據結構,每一層的索引就是每位的數字,這樣對每一個索引都要進行數學計算,而immutable-js中使用了效率更高的位運算來生成索引,將數據進行分區。好比,8899 映射爲二進制位:10001011000011,1表明此位有數據,0表明沒有數據。這樣若是每層的數組長度是7,第一層的存儲狀況能夠直接位運算 8899 >> 7 獲得 1000101(69),第二層 8899 & 127 (Math.pow(2, 7) - 1) 獲得 1000011 (67)。
位運算能夠很方便獲得每位是否存儲值的狀況,計算速度也要比數學運算快不少。性能
Map中有主要的這幾種節點類型:ArrayMapNode,ValueNode,BitmapIndexedNode,HashArrayMapNode。在每次set時,節點類型會在這幾個之間轉換。還有最終的entry數組表示的真實存儲的鍵值,entry[0]存儲了key,entry[1]存儲了value。ValueNode能夠看做葉子節點,存儲了entry值。
首先,若是存儲的鍵值對不大於8,那麼生成entry直接存儲在ArrayMapNode數組中,ArrayMapNode做爲_root節點返回,再繼續存儲值,會調用_root的update方法,也就是ArrayMapNode的update方法,若是存儲的數量大於8,會建立ValueNode,並調用它的update方法,在這裏會進行一個mergeIntoNode操做,即若是有相同索引到此處的節點,那麼要進行一次合併操做,合併後會生成BitmapIndexedNode。
BitmapIndexedNode是通過壓縮處理的層,最多存儲16個長度的內容,bitmap屬性就表示了這個層的存儲狀況。好比值爲1935909891它的存儲狀況是"1110011011000111010010000000011",最多32位,不足的話,至關於高位補0。去除0後,1的數量就是這層實際存儲的個數。爲何說它通過壓縮呢,由於這個bitmap表示了這層的存儲狀況,是長度爲32的數組的每位的存儲狀況,但這層實際存儲的數量最多隻是它的一半,爲了減小空間佔用和查找效率,就不必記錄不存儲的位了,那就有疑問了,那直接往數組裏存儲就好了,要bitmap有什麼用,咱們繼續往下看,再新增數據,使BitmapIndexedNode這層的數據量超過16,此時就會進行一次轉換expandNodes展開節點操做,將這層的結構轉爲HashArrayMapNode,這是一個長度爲32的數組,此時,bitmap記錄的位存儲狀況就是把以前的數據一個一個放到32位數組裏對應的地方,沒有值的地方就是undefined。
再日後若是HashArrayMapNode的存儲數量下降小於16了,又會進行packNodes轉爲BitmapIndexedNode。
Map中,每次set都將對應的層的節點進行類型轉換,updateNode這個方法會將受影響的節點生成新的結構返回,不影響其它層的節點,這就實現告終構共享。
這其中還有一種節點HashCollisionNode進行了hash衝突的處理。spa
在Map中爲了找到數據存儲位置,使用了不少的位操做,如今對一組map數據進行分析,看看它是如何進行計算的。這裏用到的常量,
31 和 5 在TrieUtils.js文件中:code
export const SHIFT = 5; // Resulted in best performance after ______? export const SIZE = 1 << SHIFT; export const MASK = SIZE - 1;
咱們生成500個長度的map數據結構,使它包含BitmapIndexedNode,HashArrayMapNode這幾種結構:
orm
好比咱們選取"KEY6787241"這個節點進行分析,它的hash是這樣計算出來的:
索引
咱們根據這個hash計算出它在第一層即_root下面的位置:
圖片
看到它確實放在了對應位置下的HashArrayMapNode中,那在HashArrayMapNode中12的位置是怎麼算出來的呢,咱們執行這樣的操做:
而後來看看這個bitmap 524352:
能夠看出它只有兩位保存了數據。爲了支持不定長的寬度,位置的計算是從後往前算的,那麼,存儲數據的狀況就是倒數第7位,至關於index爲6和倒數第20位,至關於index爲19,那麼咱們再計算下hash:785947024和-137605744是否在對應位置:
每深刻一層,將位右移動5位,而且與上31來算出對應位置。
那爲啥是 31 和 5 呢,在代碼中能夠看到:
就是上面對應的兩個常量。
785947024的二進制是"101110110110001001100110010000",31的二進制是"000000000000000000000000011111",&,位與操做後意思就是留下最後的5位,"10000" 16。再位移5位並位與31後就是,"01100" 12。2 ^ 5 = 32,因此5位二進制就能保存32個數,也就能知足咱們每一層最多32個的狀況。
那32是怎麼來的呢,這裏統計了位分區的查找更新效率狀況:
能夠看到64位查詢速度最快,8位更新速度最快。immutable-js選擇32是由於實際使用中,查詢的頻率要比更新多,因此選擇了查詢速度較優,更新速度不是最差的32。