1. 程式人生 > >【Scala】Vector內部結構與記憶體共享原理

【Scala】Vector內部結構與記憶體共享原理

Scala不可變集合

Scala不可變集合的設計目標是提供高效又安全的實現。這些集合中的大部分都是用高階技巧來在集合的不同版本之間“共享”記憶體。其中較長使用到的是Vector和List。
在一般的程式設計任務中,不可變集合有很多超出可變集合的優點。尤其重要的一點是不可變集合可以在多執行緒之中共享而無需加鎖。

Vector內部結構

Scala的Vector實現為一組巢狀陣列,在分割和連線時非常有效率。適用於大部分通用演算法,因為它有高效的下標計算能力,以及能夠在使用像+:和++方法時共享大部分內部結構的能力。
Vector採用分支系數為32的樹狀資料結構,分支因子是每個父節點允許擁有的最大子節點的數目。其隨機訪問(搜尋或修改)複雜度是log_32(N),使用32位整數下標時在JVM上是個效率不錯的小常量,即使對很大的N來說都近似一個常量。
Vector是個由元素的下標組成的字首樹(trie),字首樹是給定路徑上的所有子節點功用某種形式的公共鍵值。我們可以根據任何下標的二進位制形式得到查詢路徑,實現高效的元素查詢。

Vector複製過程中的結構共享原理

在實際應用中,為了保持變數的不可變性,對有用的集合進行復制通常是必要的。假設有一個包含100 000個元素的Vector,需要得到一個副本,並替換掉原Vector的第8個元素,此時如果構造一個全新的100 000個元素的Vector將會是極其低效的。
為了兼顧高效和不可變性,可以通過共享原始Vector中的不變部分,而以某種方式表示變化部分,那麼就可以高效地“建立”新Vector了。這種思想稱之為結構共享

如果其他執行緒中的程式碼正在對原始Vector做其他不同的操作,對原始的Vector的複製不會影響該操作,因為原Vector沒有被修改。這樣,只要對舊版本有一個或多個引用,就可以建立一個Vector的“歷史”版本。直到對舊版本的引用消失為止,舊版本才會被垃圾回收。

下面的圖示解釋了建立並修改副本過程中結構共享的原理:
使用#1來引用原樹的根節點:

現在假設要在2和3之間插入2.5,要建立一個新的副本,我們並不需要修改原來的樹結構,而是建立新樹。
值得注意的是,原來的樹(#1)仍然存在,但我們又建立了新的根(#2)和新的節點。建立新的樹共享重用了原來的大部分節點,這樣有助於降低修改集合的開銷。

Vector查詢原理

Scala的Vector集合非常類似於一個分支系數為32的下標字首樹。關鍵區別在於Vector用一個數組來表示分支。這使整個結構變成陣列的陣列(巢狀陣列)。
下圖是分支系數為2的二進位制Vector:

其中有三個基本素組:display0、display1和display2.這些陣列代表原始字首樹的深度。每個顯示元素(display element)都是一個更深一層的的巢狀陣列(display0是元素的陣列,display1是陣列的陣列,display2是陣列的陣列的陣列)。查詢集合元素的步驟是先判斷其深度,然後用跟字首樹一樣的方式確定元素所在的陣列

。比如找數字4,其深度為2,所以先選擇display2陣列,4的二進位制形式100,所以外層陣列是下標為1的位置上,中層陣列下標為0,最後4就位於結果陣列的下標0的位置上。

二進位制字首樹根據下標隨機取值的複雜度是log_2(n),Scala的Vector的分支系數為32,那麼訪問任何元素的時間複雜度是log_32(n),對32位的下標也就大約是7,對64位大約是13.而對於較小的集合,排序的開銷也會降低,所以訪問速度會更快。所以隨機訪問的時間複雜度與字首樹的大小成正比。

小結

Scala的Vector為32分支,這除了帶來查詢時間和修改時間可以隨集合大小伸縮外,它還提供了不錯的快取一致性,因為集合裡相近的元素有很大可能位於同一個記憶體數組裡。其高效結合不可變所帶來的執行緒安全使之成為庫裡最強大的有序集合。
Scala的序列型別中Vector和List資料結構都是很常用的,Vector的所有操作都是O(1)(常數時間),而List對於那些需要訪問頭部以為元素的操作都需要O(n)操作,所以只在頻繁執行頭尾分解的情況下,推薦使用List。