從本文開始我們一塊兒探索 Go map 裏面的奧妙吧,看看它的內在是怎麼構成的,又分別有什麼值得留意的地方?html
第一篇將探討初始化和訪問元素相關板塊,我們帶着疑問去學習,例如:golang
...算法
原文地址:深刻理解 Go map:初始化和訪問元素數組
首先咱們一塊兒看看 Go map 的基礎數據結構,先有一個大體的印象數據結構
type hmap struct { count int flags uint8 B uint8 noverflow uint16 hash0 uint32 buckets unsafe.Pointer oldbuckets unsafe.Pointer nevacuate uintptr extra *mapextra } type mapextra struct { overflow *[]*bmap oldoverflow *[]*bmap nextOverflow *bmap }
extra:原有 buckets 滿載後,會發生擴容動做,在 Go 的機制中使用了增量擴容,以下爲細項:併發
overflow
爲 hmap.buckets
(當前)溢出桶的指針地址oldoverflow
爲 hmap.oldbuckets
(舊)溢出桶的指針地址nextOverflow
爲空閒溢出桶的指針地址在這裏咱們要注意幾點,以下:函數
buckets
和 oldbuckets
也是與擴容相關的載體,通常狀況下只使用 buckets
,oldbuckets
是爲空的。但若是正在擴容的話,oldbuckets
便不爲空,buckets
的大小也會改變hint
大於 8 時,就會使用 *mapextra
作溢出桶。若小於 8,則存儲在 buckets 桶中
bucketCntBits = 3 bucketCnt = 1 << bucketCntBits ... type bmap struct { tophash [bucketCnt]uint8 }
實際 bmap 就是 buckets 中的 bucket,一個 bucket 最多存儲 8 個鍵值對性能
tophash 是個長度爲 8 的數組,代指桶最大可容納的鍵值對爲 8。學習
存儲每一個元素 hash 值的高 8 位,若是 tophash [0] <minTopHash
,則 tophash [0]
表示爲遷移進度ui
在這裏咱們留意到,存儲 k 和 v 的載體並非用 k/v/k/v/k/v/k/v
的模式,而是 k/k/k/k/v/v/v/v
的形式去存儲。這是爲何呢?
map[int64]int8
在這個例子中,若是按照 k/v/k/v/k/v/k/v
的形式存放的話,雖然每一個鍵值對的值都只佔用 1 個字節。可是卻須要 7 個填充字節來補齊內存空間。最終就會形成大量的內存 「浪費」
可是若是以 k/k/k/k/v/v/v/v
的形式存放的話,就可以解決因對齊所 "浪費" 的內存空間
所以這部分的拆分主要是考慮到內存對齊的問題,雖然相對會複雜一點,但依然值得如此設計
可能會有同窗疑惑爲何會有溢出桶這個東西?實際上在不存在哈希衝突的狀況下,去掉溢出桶,也就是隻須要桶、哈希因子、哈希算法。也能實現一個簡單的 hash table。可是哈希衝突(碰撞)是不可避免的...
而在 Go map 中當 hmap.buckets
滿了後,就會使用溢出桶接着存儲。咱們結合分析可肯定 Go 採用的是數組 + 鏈地址法解決哈希衝突
m := make(map[int32]int32)
經過閱讀源碼可得知,初始化方法有好幾種。函數原型以下:
func makemap_small() *hmap func makemap64(t *maptype, hint int64, h *hmap) *hmap func makemap(t *maptype, hint int, h *hmap) *hmap
hint
小於 8 時,會調用 makemap_small
來初始化 hmap。主要差別在因而否會立刻初始化 hash tablehint
類型爲 int64 時的特殊轉換及校驗處理,後續實質調用 makemap
func makemap(t *maptype, hint int, h *hmap) *hmap { if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) { hint = 0 } if h == nil { h = new(hmap) } h.hash0 = fastrand() B := uint8(0) for overLoadFactor(hint, B) { B++ } h.B = B if h.B != 0 { var nextOverflow *bmap h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) if nextOverflow != nil { h.extra = new(mapextra) h.extra.nextOverflow = nextOverflow } } return h }
bucket
類型,獲取其類型可以申請的最大容量大小。並對其長度 make(map[k]v, hint)
進行邊界值檢驗hint
,計算一個能夠放下 hint
個元素的桶 B
的最小值B
爲 0 將在後續懶惰分配桶,大於 0 則會立刻進行分配在這裏能夠注意到,(當 hint
大於等於 8 )第一次初始化 map 時,就會經過調用 makeBucketArray
對 buckets 進行分配。所以咱們經常會說,在初始化時指定一個適當大小的容量。可以提高性能。
若該容量過少,而新增的鍵值對又不少。就會致使頻繁的分配 buckets,進行擴容遷移等 rehash 動做。最終結果就是性能直接的降低(敲黑板)
而當 hint
小於 8 時,這種問題相對就不會凸顯的太明顯,以下:
func makemap_small() *hmap { h := new(hmap) h.hash0 = fastrand() return h }
v := m[i] v, ok := m[i]
在實現 map 元素訪問上有好幾種方法,主要是包含針對 32/64 位、string 類型的特殊處理,總的函數原型以下:
mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer) mapaccess1_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) unsafe.Pointer mapaccess2_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) (unsafe.Pointer, bool) mapaccess1_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer mapaccess2_fast32(t *maptype, h *hmap, key uint32) (unsafe.Pointer, bool) mapassign_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer mapassign_fast32ptr(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer ... mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer ...
h[key]
的指針地址,若是鍵不在 map
中,將返回對應類型的零值h[key]
的指針地址,若是鍵不在 map
中,將返回零值和布爾值用於判斷func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { ... if h == nil || h.count == 0 { return unsafe.Pointer(&zeroVal[0]) } if h.flags&hashWriting != 0 { throw("concurrent map read and map write") } alg := t.key.alg hash := alg.hash(key, uintptr(h.hash0)) m := bucketMask(h.B) b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) if c := h.oldbuckets; c != nil { if !h.sameSizeGrow() { // There used to be half as many buckets; mask down one more power of two. m >>= 1 } oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize))) if !evacuated(oldb) { b = oldb } } top := tophash(hash) for ; b != nil; b = b.overflow(t) { for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top { continue } k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) if t.indirectkey { k = *((*unsafe.Pointer)(k)) } if alg.equal(key, k) { v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) if t.indirectvalue { v = *((*unsafe.Pointer)(v)) } return v } } } return unsafe.Pointer(&zeroVal[0]) }
在上述步驟三中,提到了根據不一樣的類型計算出 hash 值,另外會計算出 hash 值的高八位和低八位。低八位會做爲 bucket index,做用是用於找到 key 所在的 bucket。而高八位會存儲在 bmap tophash 中
其主要做用是在上述步驟七中進行迭代快速定位。這樣子能夠提升性能,而不是一開始就直接用 key 進行一致性對比
在本章節,咱們介紹了 map 類型的如下知識點:
從閱讀源碼中,得知 Go 自己對於一些不一樣大小、不一樣類型的屬性,包括哈希方法都有編寫特定方法去運行。總的來講,這塊的設計隱含較多的思路,有很多點值得細細品嚐 :)
注:本文基於 Go 1.11.5