Golang 中如何優雅的使用map?


Golang中,通過哈希查找實現hash,通過鏈表解決hash沖突

map的內存模型

type hmap struct {
    count     int // map 中的元素個數,必須放在 struct 的第一個位置,因為 內置的 len 函數會從這里讀取
    flags     uint8
    B         uint8  // log_2 of # of buckets (最多可以放 loadFactor * 2^B 個元素,再多就要 hashGrow 了)
    noverflow uint16 // overflow 的 bucket 的近似數
    hash0     uint32 // hash 種子,為hash函數結果引入隨機性

    buckets    unsafe.Pointer // 2^B 大小的數組,如果 count == 0 的話,可能是 nil
    oldbuckets unsafe.Pointer // 一半大小的之前的 bucket 數組,只有在擴容過程中是非 nil
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra           // 當 key 和 value 都可以 inline 的時候,就會用這個字段
}

map中更小的單元桶,每一個桶會裝8個key,通過hash結果的高8位決定在桶里具體的位置,由hash結果的低B位決定落在哪個桶

bmap內存結構

// bucket 本體
type bmap struct {
    // tophash 是 hash 值的高 8 位
    tophash [bucketCnt]uint8
    // keys
    // values
    // overflow pointer
}

bmap是存具體key-value的地方,進一步觀察bmap底層, 將key,value分開存儲可以避免在key、value不是同一種結構時出現的內存碎片,比如map[int64]int8,在每一個key-value后面都要padding7個字節,分開存儲就只需要在最后加padding

 

當一個bmap滿了以后,會通過overflow連接一個溢出桶,實際會將每一個bmap中的overflow統一遷移到hmap的extra中,主要是為了避免gc時掃描整個hmap,所以不在單獨的桶里設置指針,而是直接讓其指向hmap.extra.overflow數組

mapextra底層結構

type mapextra struct {
    // 如果 key 和 value 都不包含指針,並且可以被 inline(<=128 字節)
    // 使用 extra 來存儲 overflow bucket,這樣可以避免 GC 掃描整個 map
    // 然而 bmap.overflow 也是個指針。這時候我們只能把這些 overflow 的指針
    // 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了
    // overflow 包含的是 hmap.buckets 的 overflow 的 bucket
    // oldoverflow 包含擴容時的 hmap.oldbuckets 的 overflow 的 bucket
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    // 指向空閑的 overflow bucket 的指針
    nextOverflow *bmap
}

map的初始化

hashTable := make(map[k]v, hint),當hint >= 4時后面會追加2^(hint-4)個桶,之后再內存頁幀對齊又追加了若干個桶

// make(map[k]v, hint), hint即預分配大小
// 不傳hint時,如用new創建個預設容量為0的map時,makemap只初始化hmap結構,不分配hash數組
func makemap(t *maptype, hint int, h *hmap) *hmap {
    // 省略部分代碼
    // 隨機hash種子
    h.hash0 = fastrand()

    // 2^h.B 為大於hint*6.5(擴容因子)的最小的2的冪
    B := uint8(0)
    // overLoadFactor(hint, B)只有一行代碼:return hint > bucketCnt && uintptr(hint) > loadFactorNum*(bucketShift(B)/loadFactorDen)
    // 即B的大小應滿足 hint <= (2^B) * 6.5
    // 一個桶能存8對key-value,所以這就表示B的初始值是保證這個map不需要擴容即可存下hint個元素對的最小的B值
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B

    // 這里分配hash數組
    if h.B != 0 {
        var nextOverflow *bmap
        h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
        // makeBucketArray()會在hash數組后面預分配一些溢出桶,
        // h.extra.nextOverflow用來保存上述溢出桶的首地址
        if nextOverflow != nil {
            h.extra = new(mapextra)
            h.extra.nextOverflow = nextOverflow
        }
    }

    return h
}

分配hash數組

// 分配hash數組
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
    base := bucketShift(b) // base代表用戶預期的桶的數量,即hash數組的真實大小
    nbuckets := base // nbuckets表示實際分配的桶的數量,>= base,這就可能會追加一些溢出桶作為溢出的預留
    if b >= 4 {
        // 這里追加一定數量的桶,並做內存對齊
        nbuckets += bucketShift(b - 4)
        sz := t.bucket.size * nbuckets
        up := roundupsize(sz)
        if up != sz {
            nbuckets = up / t.bucket.size
        }
    }

    // 后面的代碼就是申請內存空間了,此處省略
    // 這里大家可以思考下這個數組空間要怎么分配,其實就是n*sizeof(桶),所以:
        // 每個桶前面是8字節的tophash數組,然后是8個key,再是8個value,最后放一個溢出指針
        // sizeof(桶) = 8 + 8*sizeof(key) + 8*sizeof(value) + 8
    
    return buckets, nextOverflow
}

map的讀寫

value, ok := map[key]  #可以通過ok判斷map中是否存在這個key

valuePtr := map[key]   #返回value對應類型的空值,對於key和value在map中存儲的是否為指針,需要根據key,value的大小來判斷,當大於128字節時,key和value存儲的都為指針

將map作為函數參數時,會傳遞一個指針副本,對該副本操作,也同樣會影響原始map對象中的值

對於訪問key,通過key低B位確定哪個桶,通過桶高8位去與桶里每個tophash比較,只查找時,比較未找到就繼續去overflow溢出桶里找,如果是插入沒有找到,就插入第一個hashtop為empty的位置

map的key可以是除了slice,map,func 的任意類型,value可以是任意類型。

map並不是一個線程安全的數據結構,在邊遍歷邊刪除是一個同時讀寫的行為,被檢測到會直接報panic

 

map的刪除

通過delete(map, key)函數刪除key,相應的hashtop被置為empty,並沒有立即釋放內存,在指針沒有引用時會被系統gc

 

map擴容,擴容的兩個臨界點

1.由裝載因子確定,默認6.5 、即元素個數 > = 桶個數 * 6.5,表示大部分桶已滿 (B+1,將hmap的bucket數組擴容一倍)

2.由overflow的桶個數決定,當overflow溢出桶太多,代表可能是一邊插入一邊刪除,導致大量桶出現空洞,此時值存儲的非常稀疏,當bucket總數<2^15 時,overflow的bucket總數>=bucket總數;

bucket總數>=2^15,overflow的bucket >= 2^15,即認為溢出桶太多(移動bucket內容,使其更加緊密進而提高bucket利用率)縮容

當map中擁有大量hash沖突,也可能會導致overflow溢出桶數量過多,但這只是一種理論可能,golang中map中的hash隨機種子幾乎可以避免這種情況。

擴容過程:

由hmap內存結構知道了,buckets指向新的內存地址,oldbuckets仍然指向老的內存地址,golang中map的擴容是一種漸進式擴容,原有key/value不會一次性全部遷移完畢,每次只會遷移2個buckets,每一次搬遷以buckets作為單元,包括hash桶和這個桶的溢出鏈表。

對於2的縮容並不是真正的縮容,hash的容量沒有發生變化,只是讓數據更加緊湊,如果要做到真正的縮容就需要重新創建一個map,再復制。

 

參考:

1.https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/

2.https://segmentfault.com/a/1190000023879178

3.https://www.cnblogs.com/JoZSM/p/11784037.html

4.https://github.com/cch123/golang-notes/blob/master/map.md


免責聲明!

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



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