本文是深刻探究immutable.js系列的第二篇。
javascript
深刻探究immutable.js的實現機制(二) 本篇
node
上一篇咱們研究了 Immutable.js 持久化數據結構的基本實現原理,對其核心數據結構Vector Trie
進行了介紹,並着重探究了其中的位分區
機制。採用位分區
的根本緣由是爲了優化速度,而對於空間的優化, Immutable.js 是怎麼作的呢?接下來先探討下這點。git
HAMT
全稱hash array mapped trie
,其基本原理與上篇所說的Vector Trie
很是類似,不過它會對樹進行壓縮,以節約一些空間。 Immutable.js 參考了HAMT
對樹進行了高度和節點內部的壓縮。github
假設咱們有一個 2 叉 Vector Trie
,如今存了一個值,key爲110
(二進制形式), 它會被存到0
1
1
這條路徑下,以下圖:
顯然,這圖裏展現的結構已經進行了最簡單的優化,由於如今只存了一個值,因此把與110
無關的節點去掉了。還能進行什麼優化嗎?咱們發現,中間那兩個節點也是能夠去掉的,以下圖:
獲取該值時,咱們先從0
找下來,發現這直接是一個根節點,那取它存儲的值就好了。就是說在不產生混淆的狀況下,咱們能夠用盡量少的二進制位去標識這個 key 。這樣咱們就進行了高度上的壓縮,既減小了空間,又減小了查找和修改的時間。
若是要添加一個值,它的 key 結尾也是0
,該怎麼作呢?很簡單,以下圖:
咱們只要在須要的時候增長或減小節點便可。算法
上一篇咱們提到, Immutable.js 的 Trie 裏,每一個節點數組的長度是 32 ,然而在不少狀況下,這 32 個位置大部分是用不到的,這麼大的數組顯然也佔用了很大空間。使用Bitmap
,咱們就能夠對數組進行壓縮。
咱們先拿長度爲 8 的數組舉例:
咱們實際上只是用了數組的下標對 key 進行索引,這樣想數組第 五、六、7 位顯然目前是毫無做用的,那 0、二、3 呢?咱們有必要爲了一個下標 4 去維持一個長度爲5的數組嗎?咱們只須要指明「假想數組」中下標爲 1 和爲 4 的位置有數就能夠了。這裏就能夠用到bitmap
,以下:
咱們採用了一個數,以其二進制形式表達「假想的長度爲8的數組」中的佔位狀況,1 表示數組裏相應下標位置有值,0 則表示相應位置爲空。好比這個二進制數第 4 位(從右往左,從 0 開始數)如今是 1 ,就表示數組下標爲 4 的位置有值。這樣本來的長度爲 8 的數組就能夠壓縮到 2 。
注意這個數組中的元素仍是按照「假想數組」中的順序排列的,這樣咱們若要取「假想數組」中下標爲 i 的元素時,首先是判斷該位置有沒有值,如有,下一步就是獲得在它以前有幾個元素,即在二進制數裏第 i 位以前有多少位爲 1 ,假設數量爲 a ,那麼該元素在當前壓縮後的數組裏下標就是 a 。
具體操做中,咱們能夠經過bitmap & (1 << i - 1)
,獲得一個二進制數,該二進制數中只有第 i 位以前有值的地方爲 1 ,其他全爲 0 ,下面咱們只需統計該二進制數裏 1 的數量便可獲得下標。計算二進制數中 1 數量的過程被稱做popcount
,具體算法有不少,我瞭解很少就不展開了,前面點擊後是維基的地址,感興趣的能夠研究下。
下面咱們看一下這部分的源碼:編程
get(shift, keyHash, key, notSetValue) {
if (keyHash === undefined) {
keyHash = hash(key);
}
const bit = 1 << ((shift === 0 ? keyHash : keyHash >>> shift) & MASK);
const bitmap = this.bitmap;
return (bitmap & bit) === 0
? notSetValue
: this.nodes[popCount(bitmap & (bit - 1))].get(
shift + SHIFT,
keyHash,
key,
notSetValue
);
}複製代碼
popCount
方法,我把源碼也貼出來:
function popCount(x) {
x -= (x >> 1) & 0x55555555;
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x + (x >> 4)) & 0x0f0f0f0f;
x += x >> 8;
x += x >> 16;
return x & 0x7f;
}複製代碼
上一篇咱們提到了 Immutable.js 的 Vector Trie 採用了 32 做爲數組的長度,也解釋了因爲採用了位分區
,該數字只能是2的整數次冪,因此不能是 3一、33 等。但八、1六、64等等呢?這是經過實際測試得出的,見下圖:
圖中分別是查找和更新的時間,看上去彷佛 8 或 16 更好?考慮到平時的使用中,查找比更新頻次高不少,因此 Immutable.js 選擇了 32。數組
如今,咱們就能理解第一篇文章開頭的截圖了:
咱們能夠看到, map 裏主要有三種類型的節點:bash
HashArrayMapNode
,擁有的子節點數量 >16 ,擁有的數組長度爲 32BitmapIndexedNode
,擁有的子節點數量 ≤16 ,擁有的數組長度與子節點數量一致,經由 bitmap 壓縮ValueNode
,葉子節點,存儲 key 和 value此外,每一個節點彷佛都有個ownerID
屬性,這又是作什麼的呢?它涉及到 Immutable.js 中的可變數據結構。數據結構
其實能夠說 Immutable.js 中的數據結構有兩種形態,「不可變」和「可變」。雖然「不可變」是 Immutable.js 的主要優點,但「可變」形態下的操做固然效率更高。有時對於某一系列操做,咱們只須要獲得這組操做結束後的狀態,若中間的每個操做都用不可變數據結構去實現顯然有些多餘。這種情景下,咱們就可使用withMutations
方法對相應數據結構進行臨時的「可變」操做,最後再返回一個不可變的結構,這就是Transient
,好比這樣:
let map = new Immutable.Map({});
map = map.withMutations((m) => {
// 開啓Transient
m.set('a', 1); // 咱們能夠直接在m上進行修改,不須要 m = m.set('a', 1)
m.set('b', 2);
m.set('c', 3);
});
// Transient結束複製代碼
withMutations
構造臨時的可變數據結構來提升效率,好比 Map 中的
map
、
deleteAll
方法以及 Map 的構造函數。而在一個不可變數據結構中實現臨時的可變數據結構的關鍵(有點拗口XD),就是這個
ownerID
。下圖對比了使用與不使用
Transient
時的區別:
Transient
後因爲無需每次生成新的節點,效率會提升空間佔用會減小。在開啓
Transient
時,根節點會被賦與一個新的
ownerID
,在
Transient
完成前的每一步操做只需遵循下面的邏輯便可:
ownerID
與父節點的不一致,則生成新的節點,把舊節點上的值拷貝過來,其ownerID
更新爲父節點的ownerID
,而後進行相應操做;ownerID
與父節點的一致,則直接在該節點上操做;下面先咱們看下 Immutable.js 中開啓Transient
的相關源碼:
function OwnerID() {}複製代碼
function asMutable() {
return this.__ownerID ? this : this.__ensureOwner(new OwnerID());
}複製代碼
function withMutations(fn) {
const mutable = this.asMutable();
fn(mutable);
return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;
}複製代碼
ownerID
,這個
ownerID
會在接下來的操做中按照上面的邏輯使用。注意這段代碼是用 JS 的對象地址去做爲 ID ,由於每次 new 以後的對象的地址確定與以前的對象不一樣,因此用這種方法能夠很簡便高效地構造一套 ID 體系。
set
操做就會調用這個
update
方法):
update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
// ...省略前面的代碼
const isEditable = ownerID && ownerID === this.ownerID;
const newNodes = setAt(nodes, idx, newNode, isEditable);
if (isEditable) {
this.count = newCount;
this.nodes = newNodes;
return this;
}
return new HashArrayMapNode(ownerID, newCount, newNodes);
}複製代碼
ownerID
與傳進來父節點的是否一致,而後直接在節點上操做或生成新的節點。
這塊的內容就沒什麼新東西了,任何語言或庫裏對於 hashMap 的實現都需考慮到 hash 衝突的問題。咱們主要看一下 Immutable.js 是怎麼處理的。
要上一篇咱們知道了,在往 Map 裏存一對 key、value 時, Immutable.js 會先對 key 進行 hash ,根據 hash 後的值存到樹的相應位置裏。不一樣的 key 被 hash 後的結果是可能相同的,即使機率應當很小。
hash 衝突是一個很基本的問題,解決方法有不少,這裏最簡單適用的方法就是把衝突的節點擴展成一個線性結構,即數組,數組裏直接存一組組 key 和 value ,查找到此處時則遍歷該數組找到匹配的 key 。雖然這裏的時間複雜度會變成線性的,但考慮到發生 hash 衝突的機率很低,因此時間複雜度的增長能夠忽略不計。
我發現 Immutable.js 的 hash 函數對abc
和bCc
的 hash 結果都是 96354
,在同一個 map 裏用這兩個 key 就會形成 hash 衝突,咱們把這個 map log 出來以下:
Immutable.js 用了一個叫作HashCollisionNode
的節點去處理髮生衝突的鍵值,它們被放在entries
數組裏。
你們也能夠本身試試,代碼以下:
let map = new Immutable.Map({});
for (let i = 0; i < 10; i++) {
map = map.set(Math.random(), i); // 隨便塞一點別的數據
}
map = map.set('abc', 'value1');
map = map.set('bCc', 'value2');
console.log(map)複製代碼
若是文章裏有什麼問題歡迎指正。
該文章是我正在更新的深刻探究immutable.js系列的第二篇,說實話這兩篇文章寫了挺久,現成的資料不多很散,並且基本都是別的編程語言裏的。有時間和精力我會繼續更新第三篇甚至第四篇,感受仍是有些內容能夠展開。
參考:
hypirion.com/musings/und…
io-meter.com/2016/11/06/…
cdn.oreillystatic.com/en/assets/1…
infoscience.epfl.ch/record/1698…
lampwww.epfl.ch/papers/idea…
github.com/funfish/blo…
github.com/facebook/im…