Golang Map實現(一)


本文學習 Golang 的 Map 數據結構,以及map buckets 的數據組織結構。

hash 表是什么

從大學的課本里面,我們學到:hash 表其實就是將key 通過hash算法映射到數組的某個位置,然后把對應的val存放起來。
如果出現了hash沖突(也就是說,不同的key被映射到了相同的位置上時),就需要解決hash沖突。解決hash沖突的方法還是比較多的,比如說開放定址法,再哈希法,鏈地址法,公共溢出區等(復習下大學的基本知識)。

其中鏈地址法比較常見,下面是一個鏈地址法的常見模式:

Position 指通過Key 計算出的數組偏移量。例如當 Position = 6 的位置已經填滿KV后,再次插入一條相同Position的數據將通過鏈表的方式插入到該條位置之后。

在php的Array 中是這么實現的,golang中也基本是這么實現。下面我們學習下Golang中map的實現。

Golang Map 實現的數據結構

Golang的map中,首先把kv 分在了N個桶中,每個桶中的數據有8條(bucketCnt)。如果一個桶滿了(overflow),也會采用鏈地址法解決hash 的沖突。

下面是定義一個hashmap的結構體:

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的例子:
// 根據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)

一個桶的示意圖如下:

每個桶里面,可以放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:初始化和訪問


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM