這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
首先拋出一個問題,在Go中當我們想實現一個集合的時候,可以用map來實現.而map本身就可以通過”comma ok”機制來擷取該建是否存在,例如_ , ok := map["key"]
,如果沒有對應的值,ok為false,以此就可以實現集合.有時候我們會選擇map[string]bool
這類方式來定義這個集合,但是因為有了”comma ok”這個文法,還可以定義成map[string]struct{}
的形式,值不再佔用記憶體.
後者可以表示兩種狀態有或者無,而前者其實有三種狀態,有的時候表示true或者false,或者沒有.
很多時候我們會選擇map[string]struct{}
來表示集合的實現,但是這樣真得值得麼?
這裡要從map的實現說起.map的實現是一個hash表.表結構由兩個結構體表示,分別是hmap和bmap,前者表示這個map結構,後者表示了map的hash表下的bucket結構.
一切要從map的實現開始講起.
map是由桶數組組成的,每個桶的表示如下.
// A bucket for a Go map.type bmap struct { tophash [bucketCnt]uint8 // 這裡的bucetCnt是8,是個固定值,每個桶跟8個k-v對. // 先是8個key,後是8個value. // 最後是一個overflow指標指向串聯的bucket.}
而hmap表示如下,其實就是一個頭資訊.
// A header for a Go map.type hmap struct { flags uint8 // 一些標誌j B uint8 // bucket數量的log_2 hash0 uint32 // hash 種子 buckets unsafe.Pointer // buckets 數組的指標. oldbuckets unsafe.Pointer // 增長時需要被替換的數組的指標. nevacuate uintptr // 被提升的桶的數量(增長時,桶會從oldbuckets移到buckets當中) overflow *[2]*[]*bmap // 指向串聯桶的指標.}
bmap這個結構類似於C的定義,後面其實還有一些成員,但是需要動態申請(runtime自己的malloc),沒有定義.
一個bmap會有8個位元組的tophash用於定位到桶中對應的entry.每個entry表示一個k-v,這個tophash是key的hash的高位位元組.
而定位桶用的是hash的低位位元組.在go中每個類型都會有自己的hash方法.
為了防止對齊問題,所以先排8個key,再排8個value.舉個例子如果是map[int8]int64,那麼k-v排在一起的話,就會空7個位元組,非常浪費.
但是先排8個int8的話就不會出現對齊的問題.最後一個結構是桶指標,指向串聯的桶.
而整個hmap是一個bmap的數組,主要是管理資訊.
記憶體分布.
hmap的增長是依賴於負載係數的,在go裡面負載係數(loadFactor)是6.5,這個值是一個通過測試得到的比較理想的一個值.
這個值的意思表示的是,每個桶平均裝下的entry數量是6.5個,之前我們提到了每個桶的大小是8.也就是說bucket一般都不會裝滿.
如果要負載係數高,也就是桶盡量裝滿,就會導致hash碰撞率較高(可以hash到的空間不大),這樣會產生過多的overflow的bucket.
如果要負載係數低,hash碰撞率比較低,這樣會使得空間很大,導致真正利用率(存入的資料/全部bucket空間)相對變小.
所以綜合情況負載係數6.5是一個比較理想的值,這也是go現在採用的值.
這個可以通過決定增長的關鍵代碼發現:
for ; hint > bucketCnt && float32(hint) > loadFactor*float32(uintptr(1)<<B); B++ { }
2^B是桶的數量,hint是申請的map的大小,bucketCnt就是8,因為預先會分配一個桶,如果一個桶都不會超過的話就不增加了.
關鍵是hint要保證大於負載係數*桶的數量,換句話說要保證平均每個桶裝6.5個k-v能容得下hint這麼多對k-v.
上面說得是靜態分配,動態增長的時候oldbuckets是buckets的一半,也就是翻倍增長.
hmap在增長的時候會把bueckets變成oldbuckets然後再申請新的buckets.buckets中的k-v是不會移動到別的桶當中去的.
這樣保證了遍曆時候的一致性.hashmap按照range遍曆的時候是按bucket數組的一個bucket開始然後bucket的串聯bucket再回到
bucket數組的下個元素依次遍曆.
刪除非常簡單,僅僅是把對應的key和value置為空白.
現在把map的實現說清楚以後我們可以算一筆賬.假設我們的map定義為map[string]struct{}{}
,
在64bit的作業系統下面一個桶的大小是 8 + 816 + 80 + 8 = 144個位元組(string 是常量只含一個指標和一個len值).
如果是map[string]bool{}
,那麼一個桶的大小是 8 + 816 + 81 = 152個位元組.
換算下來節省的空間大概是5.2%,考慮到負載係數是6.5,換成百分比是81.25%這個程度,省8個位元組的事情完全是多餘的.
與其犧牲語義取巧節省這幾個位元組不如定義一個表示清晰的map來的更直接.
所以我的結論是map[string]struct{}
並不可取.