go map那些事兒

1.數據結構

hashmap的定義位於 src/runtime/hashmap.go 中golang

// A header for a Go map.
type hmap struct {
	count     int // 元素的個數
	flags     uint8 // 狀態標記,標記map當前狀態,是否正在寫入
	B         uint8   // 能夠最多容納 6.5 * 2 ^ B 個元素,6.5爲裝載因子
	noverflow uint16 // 溢出的個數
	hash0     uint32 // 哈希種子

	buckets    unsafe.Pointer // 桶的地址
	oldbuckets unsafe.Pointer // 舊桶的地址,用於擴容
	nevacuate  uintptr        // 遷移進度,小於nevacuate的已經遷移完成

	extra *mapextra // optional fields
}
複製代碼

桶的結構數組

// A bucket for a Go map.
type bmap struct {
        //每一個元素hash值的高8位,若是tophash[0] < minTopHash,表示這個桶的搬遷狀態
	tophash [bucketCnt]uint8
        // 接下來是8個key、8個value,可是咱們不能直接看到;爲了優化對齊,go採用了key放在一塊兒,value放在一塊兒的存儲方式,
        // 再接下來是hash衝突發生時,下一個溢出桶的地址
}
複製代碼

整個hmap結構以下markdown

image.png

2 map的建立、插入、查找、刪除、擴容

2.1 建立

map的建立比較簡單,在參數校驗以後,須要找到合適的B來申請桶的內存空間,接着即是穿件hmap這個結構,以及對它的初始化。數據結構

image.png

2.2 查找

image.png

2.3 插入

image.png

2.4 刪除

image.png

2.5 擴容

(1)判斷是否須要擴容oop

func (h *hmap) growing() bool {
    return h.oldbuckets != nil
}
複製代碼

若是oldbuckets不爲空則表示正在擴容。什麼時候h.oldbuckets不爲nil呢?在分配assign邏輯中,當沒有位置給key使用,並且知足測試條件(裝載因子>6.5或有太多溢出通)時,會觸發hashGrow邏輯:測試

func hashGrow(t *maptype, h *hmap) {
    //判斷是否須要sameSizeGrow,不然"真"擴
    bigger := uint8(1)
    if !overLoadFactor(int64(h.count), h.B) {
        bigger = 0
        h.flags |= sameSizeGrow
    }
        // 下面將buckets複製給oldbuckets
    oldbuckets := h.buckets
    newbuckets := newarray(t.bucket, 1<<(h.B+bigger))
    flags := h.flags &^ (iterator | oldIterator)
    if h.flags&iterator != 0 {
        flags |= oldIterator
    }
    // 更新hmap的變量
    h.B += bigger
    h.flags = flags
    h.oldbuckets = oldbuckets
    h.buckets = newbuckets
    h.nevacuate = 0
    h.noverflow = 0
        // 設置溢出桶
    if h.overflow != nil {
        if h.overflow[1] != nil {
            throw("overflow is not nil")
        }
// 交換溢出桶
        h.overflow[1] = h.overflow[0]
        h.overflow[0] = nil
    }
}
複製代碼

在assign和delete操做中,都會觸發擴容growWork:優化

func growWork(t *maptype, h *hmap, bucket uintptr) {
    // 搬遷舊桶,這樣assign和delete都直接在新桶集合中進行
    evacuate(t, h, bucket&h.oldbucketmask())
        //再搬遷一次搬遷過程當中的桶
    if h.growing() {
        evacuate(t, h, h.nevacuate)
    }

}
複製代碼

(2)搬遷過程ui

通常來講,新桶數組大小是原來的2倍(在!sameSizeGrow()條件下),新桶數組前半段能夠"類比"爲舊桶,對於一個key,搬遷後落入哪個索引中呢?spa

假設舊桶數組大小爲2^B, 新桶數組大小爲2*2^B,對於某個hash值X
若 X & (2^B) == 0,說明 X < 2^B,那麼它將落入與舊桶集合相同的索引xi中;
不然,它將落入xi + 2^B中
複製代碼

例如,對於舊B = 3時,hash1 = 4,hash2 = 20,其搬遷結果相似這樣。.net

image.png

代碼邏輯以下

image.png

3 難點

3.1 爲何會有等量擴容

擴容有兩個條件:1.負載因子超過閾值;2.使用了太多溢出桶。插入刪除,由於刪除沒有移動元素,除了在末尾以外,新增元素會跳過被刪的空元素。所以常常在同一個桶上插入刪除會形成這個桶的數據過於稀疏,須要等來給你擴容。

3.2 擴容爲何不是當即執行

由於map中可能會保存大量數據,一次性遷移完全部數據涉及到申請大量內存和老數據遷移,若是鎖表則會影響用戶使用,所以擴容只是作了一個標記,並無真正的申請內存和遷移數據。

3.3 擴容涉及到數據遷移,怎麼遷移

遷移數據是增量的過程,即下次放問到了哪一個元素就遷移那個元素,遷移是按桶爲單位,直到全部的桶都遷移完成纔算遷移完。

當訪問某個桶的時候會判斷是否正在遷移,若是訪問老桶,若是是雙倍容量擴容,則把桶的大小除以2,訪問老桶,這裏須要判斷老桶是否遷移完成,若是遷移完成了則訪問新桶evacuated(oldb)爲true則表示遷移完成

func evacuated(b *bmap) bool {
	h := b.tophash[0]
	return h > emptyOne && h < minTopHash
}
複製代碼

當插入某個元素,若是正在遷移,則遷移這個桶,而且當前元素插入新桶

一個桶中的元素是從頭至尾遷移,會從新計算它的位置。原來在x處的元素可能到了新桶的x位置,也多是在2^B+x的位置

4 參考

【1】Golang map底層實現原理解析
【2】解剖Go語言map底層實現
【3】哈希表
【4】Golang Map實現原理
【5】Golang源碼解析

相關文章
相關標籤/搜索