深入理解 Go map:初始化和訪問元素

從本文開始咱們一起探索 Go map 里面的奧妙吧,看看它的內(nèi)在是怎么構(gòu)成的,又分別有什么值得留意的地方?

第一篇將探討初始化和訪問元素相關(guān)板塊,咱們帶著疑問去學(xué)習(xí),例如:

  • 初始化的時(shí)候會(huì)馬上分配內(nèi)存嗎?
  • 底層數(shù)據(jù)是如何存儲(chǔ)的?
  • 底層是如何使用 key 去尋找數(shù)據(jù)的?
  • 底層是用什么方式解決哈希沖突的?
  • 數(shù)據(jù)類型那么多,底層又是怎么處理的呢?

...

原文地址:深入理解 Go map:初始化和訪問元素

數(shù)據(jù)結(jié)構(gòu)

首先我們一起看看 Go map 的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),先有一個(gè)大致的印象

image

hmap

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
}
  • count:map 的大小,也就是 len() 的值。代指 map 中的鍵值對(duì)個(gè)數(shù)
  • flags:狀態(tài)標(biāo)識(shí),主要是 goroutine 寫入和擴(kuò)容機(jī)制的相關(guān)狀態(tài)控制。并發(fā)讀寫的判斷條件之一就是該值
  • B:桶,最大可容納的元素?cái)?shù)量,值為 負(fù)載因子(默認(rèn) 6.5) * 2 ^ B,是 2 的指數(shù)
  • noverflow:溢出桶的數(shù)量
  • hash0:哈希因子
  • buckets:保存當(dāng)前桶數(shù)據(jù)的指針地址(指向一段連續(xù)的內(nèi)存地址,主要存儲(chǔ)鍵值對(duì)數(shù)據(jù))
  • oldbuckets,保存舊桶的指針地址
  • nevacuate:遷移進(jìn)度
  • extra:原有 buckets 滿載后,會(huì)發(fā)生擴(kuò)容動(dòng)作,在 Go 的機(jī)制中使用了增量擴(kuò)容,如下為細(xì)項(xiàng):
    • overflowhmap.buckets (當(dāng)前)溢出桶的指針地址
    • oldoverflowhmap.oldbuckets (舊)溢出桶的指針地址
    • nextOverflow 為空閑溢出桶的指針地址

在這里我們要注意幾點(diǎn),如下:

  1. 如果 keys 和 values 都不包含指針并且允許內(nèi)聯(lián)的情況下。會(huì)將 bucket 標(biāo)識(shí)為不包含指針,使用 extra 存儲(chǔ)溢出桶就可以避免 GC 掃描整個(gè) map,節(jié)省不必要的開銷
  2. 在前面有提到,Go 用了增量擴(kuò)容。而 bucketsoldbuckets 也是與擴(kuò)容相關(guān)的載體,一般情況下只使用 bucketsoldbuckets 是為空的。但如果正在擴(kuò)容的話,oldbuckets 便不為空,buckets 的大小也會(huì)改變
  3. 當(dāng) hint 大于 8 時(shí),就會(huì)使用 *mapextra 做溢出桶。若小于 8,則存儲(chǔ)在 buckets 桶中

bmap

image
bucketCntBits = 3
bucketCnt     = 1 << bucketCntBits
...
type bmap struct {
    tophash [bucketCnt]uint8
}
  • tophash:key 的 hash 值高 8 位
  • keys:8 個(gè) key
  • values:8 個(gè) value
  • overflow:下一個(gè)溢出桶的指針地址(當(dāng) hash 沖突發(fā)生時(shí))

實(shí)際 bmap 就是 buckets 中的 bucket,一個(gè) bucket 最多存儲(chǔ) 8 個(gè)鍵值對(duì)

tophash

tophash 是個(gè)長度為 8 的數(shù)組,代指桶最大可容納的鍵值對(duì)為 8。

存儲(chǔ)每個(gè)元素 hash 值的高 8 位,如果 tophash [0] <minTopHash,則 tophash [0] 表示為遷移進(jìn)度

keys 和 values

在這里我們留意到,存儲(chǔ) k 和 v 的載體并不是用 k/v/k/v/k/v/k/v 的模式,而是 k/k/k/k/v/v/v/v 的形式去存儲(chǔ)。這是為什么呢?

map[int64]int8

在這個(gè)例子中,如果按照 k/v/k/v/k/v/k/v 的形式存放的話,雖然每個(gè)鍵值對(duì)的值都只占用 1 個(gè)字節(jié)。但是卻需要 7 個(gè)填充字節(jié)來補(bǔ)齊內(nèi)存空間。最終就會(huì)造成大量的內(nèi)存 “浪費(fèi)”

image

但是如果以 k/k/k/k/v/v/v/v 的形式存放的話,就能夠解決因?qū)R所 "浪費(fèi)" 的內(nèi)存空間

因此這部分的拆分主要是考慮到內(nèi)存對(duì)齊的問題,雖然相對(duì)會(huì)復(fù)雜一點(diǎn),但依然值得如此設(shè)計(jì)

image

overflow

可能會(huì)有同學(xué)疑惑為什么會(huì)有溢出桶這個(gè)東西?實(shí)際上在不存在哈希沖突的情況下,去掉溢出桶,也就是只需要桶、哈希因子、哈希算法。也能實(shí)現(xiàn)一個(gè)簡單的 hash table。但是哈希沖突(碰撞)是不可避免的...

而在 Go map 中當(dāng) hmap.buckets 滿了后,就會(huì)使用溢出桶接著存儲(chǔ)。我們結(jié)合分析可確定 Go 采用的是數(shù)組 + 鏈地址法解決哈希沖突

image

初始化

用法

m := make(map[int32]int32)

函數(shù)原型

通過閱讀源碼可得知,初始化方法有好幾種。函數(shù)原型如下:

func makemap_small() *hmap
func makemap64(t *maptype, hint int64, h *hmap) *hmap
func makemap(t *maptype, hint int, h *hmap) *hmap
  • makemap_small:當(dāng) hint 小于 8 時(shí),會(huì)調(diào)用 makemap_small 來初始化 hmap。主要差異在于是否會(huì)馬上初始化 hash table
  • makemap64:當(dāng) hint 類型為 int64 時(shí)的特殊轉(zhuǎn)換及校驗(yàn)處理,后續(xù)實(shí)質(zhì)調(diào)用 makemap
  • makemap:實(shí)現(xiàn)了標(biāo)準(zhǔn)的 map 初始化動(dòng)作

源碼

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
}
  • 根據(jù)傳入的 bucket 類型,獲取其類型能夠申請的最大容量大小。并對(duì)其長度 make(map[k]v, hint) 進(jìn)行邊界值檢驗(yàn)
  • 初始化 hmap
  • 初始化哈希因子
  • 根據(jù)傳入的 hint,計(jì)算一個(gè)可以放下 hint 個(gè)元素的桶 B 的最小值
  • 分配并初始化 hash table。如果 B 為 0 將在后續(xù)懶惰分配桶,大于 0 則會(huì)馬上進(jìn)行分配
  • 返回初始化完畢的 hmap

在這里可以注意到,(當(dāng) hint 大于等于 8 )第一次初始化 map 時(shí),就會(huì)通過調(diào)用 makeBucketArray 對(duì) buckets 進(jìn)行分配。因此我們常常會(huì)說,在初始化時(shí)指定一個(gè)適當(dāng)大小的容量。能夠提升性能。

若該容量過少,而新增的鍵值對(duì)又很多。就會(huì)導(dǎo)致頻繁的分配 buckets,進(jìn)行擴(kuò)容遷移等 rehash 動(dòng)作。最終結(jié)果就是性能直接的下降(敲黑板)

而當(dāng) hint 小于 8 時(shí),這種問題相對(duì)就不會(huì)凸顯的太明顯,如下:

func makemap_small() *hmap {
    h := new(hmap)
    h.hash0 = fastrand()
    return h
}

圖示

image

訪問

用法

v := m[i]
v, ok := m[i]

函數(shù)原型

在實(shí)現(xiàn) map 元素訪問上有好幾種方法,主要是包含針對(duì) 32/64 位、string 類型的特殊處理,總的函數(shù)原型如下:

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 中,將返回對(duì)應(yīng)類型的零值
  • 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。若是則返回零值
  • 判斷當(dāng)前是否并發(fā)讀寫 map,若是則拋出異常
  • 根據(jù) key 的不同類型調(diào)用不同的 hash 方法計(jì)算得出 hash 值
  • 確定 key 在哪一個(gè) bucket 中,并得到其位置
  • 判斷是否正在發(fā)生擴(kuò)容(h.oldbuckets 是否為 nil),若正在擴(kuò)容,則到老的 buckets 中查找(因?yàn)?buckets 中可能還沒有值,搬遷未完成),若該 bucket 已經(jīng)搬遷完畢。則到 buckets 中繼續(xù)查找
  • 計(jì)算 hash 的 tophash 值(高八位)
  • 根據(jù)計(jì)算出來的 tophash,依次循環(huán)對(duì)比 buckets 的 tophash 值(快速試錯(cuò))
  • 如果 tophash 匹配成功,則計(jì)算 key 的所在位置,正式完整的對(duì)比兩個(gè) key 是否一致
  • 若查找成功并返回,若不存在,則返回零值

在上述步驟三中,提到了根據(jù)不同的類型計(jì)算出 hash 值,另外會(huì)計(jì)算出 hash 值的高八位和低八位。低八位會(huì)作為 bucket index,作用是用于找到 key 所在的 bucket。而高八位會(huì)存儲(chǔ)在 bmap tophash 中

其主要作用是在上述步驟七中進(jìn)行迭代快速定位。這樣子可以提高性能,而不是一開始就直接用 key 進(jìn)行一致性對(duì)比

圖示

image

總結(jié)

在本章節(jié),我們介紹了 map 類型的以下知識(shí)點(diǎn):

  • map 的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
  • 初始化 map
  • 訪問 map

從閱讀源碼中,得知 Go 本身對(duì)于一些不同大小、不同類型的屬性,包括哈希方法都有編寫特定方法去運(yùn)行。總的來說,這塊的設(shè)計(jì)隱含較多的思路,有不少點(diǎn)值得細(xì)細(xì)品嘗 :)

注:本文基于 Go 1.11.5

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,708評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,774評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,650評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容