原文標題:Why doesn't Rust's BTreeMap have a with_capacity() method?
原文連結:https://www.nicolas-hahn.com/2020/11/30/btreemap-with-capacity/
公眾號: Rust 碎碎念
翻譯 by: Praying
宣告:我發現這裡已經有一篇解釋,但是我認為它有點不太好理解,所以我希望我寫的這篇文章能夠更容易理解一些。
Rust 的 HashMap(以及 HashSet 和 Vec)集合都提供了一個初始化方法—— fn with_capacity(capacity: usize)
,該方法預先分配足夠的記憶體空間以儲存capacity
個元素。為什麼 BTreeMap(和 BTreeSet)沒有這個方法呢?
答案就在於這兩個結構體在記憶體中佈局的不同。簡而言之,HashMap,就像 Vec,使用了一個 array(一個連續的記憶體塊),要求在 O(1)的時間內插通過索引插入和查詢元素。在 Vec 中,這很明顯,但是在 HashMap 中,key 是被 hash 之後轉為 value 在陣列中的索引。
讓我們來看一個已經存入四條記錄的 HashMap(簡單起見,我打算忽略真實的實現細節,比如 hash 碰撞時的裝桶(bucket))。它在本質上來講是一個擁有四個元素的陣列。下面是一個表示存有三條記錄的 HashMap 的記憶體表示(每個格子為一個位元組),以及若干個方格(亮綠色是記憶體中被填充的位元組,深綠色是空的,但是被結構體保留)。
我們插入兩個元素。現在我們需要分類更多記憶體以存放第五個元素。常見的實現是將陣列的大小翻倍(以便於我們不必在每次插入時都進行分配)。在理想情況下,我們可以直接使用記憶體中接下來的四個位元組。
(事實上,元素是不可能像這樣被連續存放的,因為 hasher 會以近似隨機分佈的方式輸入一個陣列的索引)。
儘管如此,如果接下來的四個位元組已經被分配給其他的結構體了會怎麼樣呢?
在這種情況下,我們需要把整個 HashMap 移動到記憶體中的某個可以容下八條記錄的位置。不同於額外分配四個位元組 ,這次我們需要先分配八個位元組(將資料拷貝過去),然後析構原來的四個位元組,這個開銷就比較高了。
這裡就是with_capacity()
出現的原因。如果我們預先知道我們至少會有五個元素,那麼預先分配八個位元組就能讓我們不必反覆析構和重分配,這也是with_capacity()
所做的事情。
那麼 BTreeMap 為什麼沒有這個方法呢?來看一下BTree 是如何工作的。在下面這個例子中,我打算把它簡化為一個普通的二分查詢樹。它們倆之間的本質區別在於,BST(二分查詢樹)的每個節點有一個值和兩個指標,但是一個 BTree 的每個節點擁有一組值和一組指標:
這裡為了便於上面的解釋,它們暫時可以被視作等同。
BST 的每個節點由一個值和兩個分別指向左右子節點的指標組成。下面是一個只有一個節點和值的BTreeMap
(亮藍色)。第二個和第三個暗藍色的位元組被保留用於指向子節點的指標,目前是空的。
當一個元素被插入時,一個新節點會被建立並且會分配屬於它的記憶體。因為指標可以指向記憶體中的任意地址,所以不必要求節點像 HashMap 那樣在記憶體中儲存為連續的位元組。如果我們打算插入一條新記錄,會如下圖所示:
我們可以把這條新記錄放在記憶體中任意擁有三個位元組的自由空間的位置。一個 BTreeMap 可以遍佈在程式的記憶體各處,因為我們不必把記錄連續存放。這意味著,我們將從不需要析構和重分配空間以拷貝記錄(元素),所以我們不會在 BTreeMap 初始化時通過預先分配額外的記憶體空間來節省某些環節(在整個程式執行時)。
如果你明確想要預先分配以節省插入過程的時間,或者如果這時的延遲代價很大, BTreeMap::with_capacity()
或許會有意義。但我想這種用例對於標準庫函式而言過於特殊。在有用(usefulness)和臃腫之間存在一個微妙的平衡。
歡迎關注公眾號:Rust碎碎念,獲取更多好文章