摘要:但是哈希沖突碰撞是不可避免的而在中當滿了后,就會使用溢出桶接著存儲。并對其長度進行邊界值檢驗初始化初始化哈希因子根據傳入的,計算一個可以放下個元素的桶的最小值分配并初始化。
從本文開始咱們一起探索 Go map 里面的奧妙吧,看看它的內在是怎么構成的,又分別有什么值得留意的地方?
第一篇將探討初始化和訪問元素相關板塊,咱們帶著疑問去學習,例如:
初始化的時候會馬上分配內存嗎?
底層數據是如何存儲的?
底層是如何使用 key 去尋找數據的?
底層是用什么方式解決哈希沖突的?
數據類型那么多,底層又是怎么處理的呢?
...
原文地址:深入理解 Go map:初始化和訪問元素
數據結構首先我們一起看看 Go map 的基礎數據結構,先有一個大致的印象
hmaptype 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 }
count:map 的大小,也就是 len() 的值。代指 map 中的鍵值對個數
flags:狀態標識,主要是 goroutine 寫入和擴容機制的相關狀態控制。并發讀寫的判斷條件之一就是該值
B:桶,最大可容納的元素數量,值為 負載因子(默認 6.5) * 2 ^ B,是 2 的指數
noverflow:溢出桶的數量
hash0:哈希因子
buckets:保存當前桶數據的指針地址(指向一段連續的內存地址,主要存儲鍵值對數據)
oldbuckets,保存舊桶的指針地址
nevacuate:遷移進度
extra:原有 buckets 滿載后,會發生擴容動作,在 Go 的機制中使用了增量擴容,如下為細項:
overflow 為 hmap.buckets (當前)溢出桶的指針地址
oldoverflow 為 hmap.oldbuckets (舊)溢出桶的指針地址
nextOverflow 為空閑溢出桶的指針地址
在這里我們要注意幾點,如下:
如果 keys 和 values 都不包含指針并且允許內聯的情況下。會將 bucket 標識為不包含指針,使用 extra 存儲溢出桶就可以避免 GC 掃描整個 map,節省不必要的開銷
在前面有提到,Go 用了增量擴容。而 buckets 和 oldbuckets 也是與擴容相關的載體,一般情況下只使用 buckets,oldbuckets 是為空的。但如果正在擴容的話,oldbuckets 便不為空,buckets 的大小也會改變
當 hint 大于 8 時,就會使用 *mapextra 做溢出桶。若小于 8,則存儲在 buckets 桶中
bmapbucketCntBits = 3 bucketCnt = 1 << bucketCntBits ... type bmap struct { tophash [bucketCnt]uint8 }
tophash:key 的 hash 值高 8 位
keys:8 個 key
values:8 個 value
overflow:下一個溢出桶的指針地址(當 hash 沖突發生時)
實際 bmap 就是 buckets 中的 bucket,一個 bucket 最多存儲 8 個鍵值對
tophashtophash 是個長度為 8 的數組,代指桶最大可容納的鍵值對為 8。
存儲每個元素 hash 值的高 8 位,如果 tophash [0]
在這里我們留意到,存儲 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 的形式存放的話,就能夠解決因對齊所 "浪費" 的內存空間
因此這部分的拆分主要是考慮到內存對齊的問題,雖然相對會復雜一點,但依然值得如此設計
overflow可能會有同學疑惑為什么會有溢出桶這個東西?實際上在不存在哈希沖突的情況下,去掉溢出桶,也就是只需要桶、哈希因子、哈希算法。也能實現一個簡單的 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
makemap_small:當 hint 小于 8 時,會調用 makemap_small 來初始化 hmap。主要差異在于是否會馬上初始化 hash table
makemap64:當 hint 類型為 int64 時的特殊轉換及校驗處理,后續實質調用 makemap
makemap:實現了標準的 map 初始化動作
源碼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) 進行邊界值檢驗
初始化 hmap
初始化哈希因子
根據傳入的 hint,計算一個可以放下 hint 個元素的桶 B 的最小值
分配并初始化 hash table。如果 B 為 0 將在后續懶惰分配桶,大于 0 則會馬上進行分配
返回初始化完畢的 hmap
在這里可以注意到,(當 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 ...
mapaccess1:返回 h[key] 的指針地址,如果鍵不在 map 中,將返回對應類型的零值
mapaccess2:返回 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]) }
判斷 map 是否為 nil,長度是否為 0。若是則返回零值
判斷當前是否并發讀寫 map,若是則拋出異常
根據 key 的不同類型調用不同的 hash 方法計算得出 hash 值
確定 key 在哪一個 bucket 中,并得到其位置
判斷是否正在發生擴容(h.oldbuckets 是否為 nil),若正在擴容,則到老的 buckets 中查找(因為 buckets 中可能還沒有值,搬遷未完成),若該 bucket 已經搬遷完畢。則到 buckets 中繼續查找
計算 hash 的 tophash 值(高八位)
根據計算出來的 tophash,依次循環對比 buckets 的 tophash 值(快速試錯)
如果 tophash 匹配成功,則計算 key 的所在位置,正式完整的對比兩個 key 是否一致
若查找成功并返回,若不存在,則返回零值
在上述步驟三中,提到了根據不同的類型計算出 hash 值,另外會計算出 hash 值的高八位和低八位。低八位會作為 bucket index,作用是用于找到 key 所在的 bucket。而高八位會存儲在 bmap tophash 中
其主要作用是在上述步驟七中進行迭代快速定位。這樣子可以提高性能,而不是一開始就直接用 key 進行一致性對比
圖示 總結在本章節,我們介紹了 map 類型的以下知識點:
map 的基礎數據結構
初始化 map
訪問 map
從閱讀源碼中,得知 Go 本身對于一些不同大小、不同類型的屬性,包括哈希方法都有編寫特定方法去運行。總的來說,這塊的設計隱含較多的思路,有不少點值得細細品嘗 :)
注:本文基于 Go 1.11.5
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/30943.html
摘要:我相信這樣你能更好的讀懂這篇文章原文地址深入理解賦值和擴容遷移哈希函數哈希函數,又稱散列算法散列函數。而一個好的哈希函數,應當盡量少的出現哈希沖突,以此保證操作哈希表的時間復雜度但是哈希沖突在目前來講,是無法避免的。 概要 在 上一章節 中,數據結構小節里講解了大量基礎字段,可能你會疑惑需要 #&(!……#(!¥! 來干嘛?接下來我們一起簡單了解一下基礎概念。再開始研討今天文章的重點內...
摘要:小白前端一枚,最近在研究,記錄自己學習過程中的一些筆記,以及自己的理解。此外,結構體也支持嵌套。在函數聲明時,在函數名前放上一個變量,這個變量稱為方法的接收器,一般是結構體類型的。 小白前端一枚,最近在研究golang,記錄自己學習過程中的一些筆記,以及自己的理解。 go中包的依賴管理 go中的切片 byte 和 string go中的Map go中的struct結構體 go中的方...
摘要:當然,哈希查找表的平均查找效率是,如果哈希函數設計的很好,最壞的情況基本不會出現。選擇函數主要考察的是兩點性能碰撞概率。再用哈希值的高位,找到此在中的位置,這是在尋找已有的。這篇文章主要講 map 的賦值、刪除、查詢、擴容的具體執行過程,仍然是從底層的角度展開。結合源碼,看完本文一定會徹底明白 map 底層原理。 我要說明的是,這里對 map 的基本用法涉及比較少,我相信可以通過閱讀其他入門...
摘要:當然,哈希查找表的平均查找效率是,如果哈希函數設計的很好,最壞的情況基本不會出現。選擇函數主要考察的是兩點性能碰撞概率。再用哈希值的高位,找到此在中的位置,這是在尋找已有的。這篇文章主要講 map 的賦值、刪除、查詢、擴容的具體執行過程,仍然是從底層的角度展開。結合源碼,看完本文一定會徹底明白 map 底層原理。 我要說明的是,這里對 map 的基本用法涉及比較少,我相信可以通過閱讀其他入門...
摘要:我們先將上面的接口解析放放,先看下是如何初始化的路徑定義了,再看路徑定義空的創建,用于不同版本對象轉換增加一些轉換函數上面就創建了一個空的。其實就是向添加了轉換函數,比如將轉換為,將轉換為。 源碼版本 Kubernetes v1.5.0 簡介 k8s里面有各種資源,如Pod、Service、RC、namespaces等資源,用戶操作的其實也就是這一大堆資源。但這些資源并不是雜亂無章的,...
閱讀 2458·2021-09-27 13:36
閱讀 2163·2019-08-29 18:47
閱讀 2129·2019-08-29 15:21
閱讀 1394·2019-08-29 11:14
閱讀 1979·2019-08-28 18:29
閱讀 1623·2019-08-28 18:04
閱讀 568·2019-08-26 13:58
閱讀 3206·2019-08-26 12:12