1. 程式人生 > >Golang Map實現(一)

Golang Map實現(一)

本文學習 Golang 的 Map 資料結構,以及map buckets 的資料組織結構。 # hash 表是什麼 從大學的課本里面,我們學到:hash 表其實就是將key 通過hash演算法對映到陣列的某個位置,然後把對應的val存放起來。 如果出現了hash衝突(也就是說,不同的key被對映到了相同的位置上時),就需要解決hash衝突。解決hash衝突的方法還是比較多的,比如說開放定址法,再雜湊法,鏈地址法,公共溢位區等(複習下大學的基本知識)。 其中鏈地址法比較常見,下面是一個鏈地址法的常見模式: ![](https://img2020.cnblogs.com/blog/527714/202004/527714-20200426093755804-765999692.jpg) Position 指通過Key 計算出的陣列偏移量。例如當 Position = 6 的位置已經填滿KV後,再次插入一條相同Position的資料將通過連結串列的方式插入到該條位置之後。 在php的Array 中是這麼實現的,golang中也基本是這麼實現。下面我們學習下Golang中map的實現。 ## Golang Map 實現的資料結構 Golang的map中,首先把kv 分在了N個桶中,每個桶中的資料有8條(bucketCnt)。如果一個桶滿了(overflow),也會採用鏈地址法解決hash 的衝突。 下面是定義一個hashmap的結構體: ```golang type hmap struct { // 長度 count int // map 的標識, 下方做了定義 flags uint8 // 實際buckets 的長度為 2 ^ B B uint8 // 從bucket中溢位的數量,(存在extra 裡面) noverflow uint16 // hash 種子,做key 雜湊的時候會用到 hash0 uint32 // 儲存 buckets 的地方 buckets unsafe.Pointer // 遷移時oldbuckets中存放部分buckets 的資料 oldbuckets unsafe.Pointer // 遷移的數量 nevacuate uintptr // 一些額外的欄位,在做溢位處理以及資料增長的時候會用到 extra *mapextra } const ( // 有一個迭代器在使用buckets iterator = 1 // 有一個迭代器在使用oldbuckets oldIterator = 2 // 併發寫,通過這個標識報panic hashWriting = 4 sameSizeGrow = 8 ) type mapextra struct { overflow *[]*bmap oldoverflow *[]*bmap nextOverflow *bmap } type bmap struct { tophash [bucketCnt]uint8 } ``` 表中除了對基本的hash資料結構做了定義外,還對資料遷移、擴容等操作做了定義,這裡我們可以忽略,等學習到時我們再深入瞭解。 ## 深入 桶列表 (buckets) buckets 欄位中是儲存桶資料的地方。正常會一次申請至少2^N長度的陣列,陣列中每個元素就是一個桶。N 就是結構體中的B。這裡面要注意以下幾點: 1. **為啥是2的冪次方** 為了做完hash後,通過掩碼的方式取到陣列的偏移量, 省掉了不必要的計算。 2. **B 這個數是怎麼確定的** 這個和我們map中要存放的資料量是有很大關係的。我們在建立map的時候來詳述。 3. **bucket 的偏移是怎麼計算的** hash 方法有多個,在 runtime/alg.go 裡面定義了。不同的型別用不同的hash演算法。算出來是一個uint32的一個hash 碼,通過和B取掩碼,就找到了bucket的偏移了。下面是取對應bucket的例子: ```golang // 根據key的型別取相應的hash演算法 alg := t.key.alg hash := alg.hash(key, uintptr(h.hash0)) // 根據B拿到一個掩碼 m := bucketMask(h.B) // 通過掩碼以及hash指,計算偏移得到一個bucket b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) ``` ## 深入 桶 (bucket) 一個桶的示意圖如下: ![](https://img2020.cnblogs.com/blog/527714/202004/527714-20200426093845638-584258581.jpg) 每個桶裡面,可以放8個k,8個v,還有一個overflow指標(就是上面的next),用來指向下一個bucket 的地址。在每個bucket的頭部,還會放置一個tophash,也就是bmap 結構體。這個數組裡面存放的是key的hash值,用來對比我們key生成的hash和存出的hash是否一致(當然除了這個還有其他的用途,後面講資料訪問的時候會講到)。 tophash中的資料,是從計算的hash值裡面擷取的。獲取bucket 是用的低bit位的hash,tophash 使用的是高bit位的hash值(8位) 1. **為啥bucket 一次要存8個kv,而不是一個kv放一個bucket,然後鏈地址法做處理就OK了** 據我分析,有幾點原因: a, 一次分配8個kv的空間,可以減少記憶體的分配頻次; b,減少了overflow指標的記憶體佔用,比如說8個kv,採用一個一個儲存的話,需要8 * 8B (64位機) = 64B的資料存下一個的地址,而採用go實現的這種方式,只需要 8B + 8B (bmap的大小) = 16B 的資料就可以了。 2. **為啥需要用tophash** 一般的hash 實現邏輯是直接和key比較,如果比較成功,這找到相應key的資料。但是這裡用到了tophash,好處是可以減少key的比較成本(畢竟key 不一定都是整數形式存在的) 3. **為啥是8個** 8 * 8B = 64B 整好是64位機的一個最小定址空間,不過可以通過修改原始碼自定義吧。 4. **為什麼key 和val 要分開放** 這個也比較好理解,key 和val 都是使用者可以自定義的。如果key是定長的(比如是數字,或者 指標之類的,大概率是這樣。)記憶體是比較整齊的,利於定址吧。 ## 技術總結 golang 實現的map比樸素的hashmap 在很多方面都有優化。 1. 使用掩碼方式獲取偏移,減少判斷。 2. bucket 儲存方式的優化。 3. 通過tophash 先進行一次比較,減少key 比較的成本。 4. 當然,有一點是不太明白的,為啥 overflow 指標要放在 kv 後面? 放在tophash 之後的位置豈不是更完美? 今天的作業就交完了。下一篇將學習golang map的資料初始化實現。 ## 參考 [1] [深入理解 Go map:初始化和訪問](https://eddycjy.com/posts/go/map/2019-03-05-map-a