golang源碼之map

一、概述

在golang中map類型是實(shí)際開發(fā)過程中經(jīng)常使用到的數(shù)據(jù)類型,比如在微服務(wù)框架中存放[client,service]這樣的映射汁展,還有在實(shí)現(xiàn)長連接通信[client_id,connection]昙啄,再例如rpc框架中[service_id, service_impl]等等。在其實(shí)際的定義中:

  • 1获三、底層
    map實(shí)際上是一個(gè)hashtable旁蔼,存放的數(shù)據(jù)是被放置到一個(gè)數(shù)組buckets.每個(gè)bucket最多能夠存放8個(gè)key/value對(duì)锨苏。而對(duì)應(yīng)的數(shù)據(jù)則根據(jù)其hash(key)的bits低階位來選擇對(duì)應(yīng)的bucket,而對(duì)應(yīng)的高階位則用來區(qū)分在同一個(gè)bucket中不同的key-value內(nèi)容棺聊。一旦當(dāng)前的bucket里面的鍵值對(duì)個(gè)數(shù)超過8個(gè)伞租,則會(huì)通過鏈表的方式拓展其他的bucket。
  • 2限佩、擴(kuò)容
    當(dāng)hashtable增長需要擴(kuò)容時(shí)葵诈,則通過分配一個(gè)容量是當(dāng)數(shù)組buckets兩倍的新數(shù)組來存放鍵值對(duì),并將舊的buckets中的key-value逐步copy到新的buckets中犀暑。需要注意一點(diǎn):當(dāng)map進(jìn)行擴(kuò)容時(shí)其對(duì)應(yīng)的iterator遍歷舊的table驯击,同時(shí)也會(huì)檢查新的table,防止對(duì)應(yīng)的bucket移到新的table中耐亏。
  • 3徊都、查詢
    可以通過Map iterator遍歷buckets數(shù)組,按照遍歷的順序返回keys(當(dāng)前bucket--->chain序號(hào)[當(dāng)存放鍵值對(duì)>8個(gè)時(shí)會(huì)以鏈表的存放到擴(kuò)展的bucket]--->bucket索引)广辰,在實(shí)現(xiàn)map iterator為了保證其iterator語義暇矫,并不會(huì)在bucket中移動(dòng)keys,一旦這樣做了則會(huì)導(dǎo)致keys會(huì)被獲取到0次或2次择吊。
  • 4李根、負(fù)載系數(shù)
    在實(shí)際的應(yīng)用我們會(huì)面臨一個(gè)問題:當(dāng)table出現(xiàn)太多overflow情況就需要擴(kuò)展更多的bucket來支撐,反之几睛,若是太小的話又會(huì)造成空間的浪費(fèi)房轿。
    接下來我們先通過源碼了解下map的實(shí)現(xiàn)。

二所森、源碼

type hmap struct {
    count     int // 元素格式
    flags     uint8
    B      uint8  // 包含的buckets數(shù)loadFactor * 2^B items)
    noverflow uint16 // overflow時(shí)拓展的buckets數(shù)
    hash0     uint32 // hash種子

    buckets    unsafe.Pointer // buckets數(shù)組指針
    oldbuckets unsafe.Pointer // 擴(kuò)容時(shí)用于復(fù)制的數(shù)組
    nevacuate  uintptr   // 擴(kuò)容時(shí)copy到新table的buckets數(shù)

    extra *mapextra // 可選(見下文)
}

首先呢囱持,map是一個(gè)由若干個(gè)bucket(對(duì)應(yīng)bmap)組成的數(shù)組,并且每個(gè)bucket存放不超過8個(gè)鍵值對(duì)key-value的元素焕济,則由key通過哈希算法將其歸入不同的bucket中纷妆。一旦某個(gè)bucket中元素超過8個(gè)元素就會(huì)觸發(fā)overflow,hmap則通過extra字段對(duì)應(yīng)的mapextra的overflow字段來拓展該bucket晴弃。

bmap結(jié)構(gòu):bmap就是前面在hmap中提到的bucket

type bmap struct {  
    tophash [bucketCnt]uint8
}

從源碼可以了解到tophash包含了在一個(gè)bucket中每個(gè)key對(duì)應(yīng)的hash值的高階位部分掩幢,一旦tophash[0] < minTopHash則tophash[0]就變成evacuation狀態(tài)。
在這里tophash通過記錄當(dāng)前bucket中8個(gè)key的hash值的高8位上鞠,在每次查找對(duì)應(yīng)的key時(shí)不需要做全等判斷际邻,提高查找速度。
在每個(gè)bucket(bmap)中存放的數(shù)據(jù)格式:key1key2...keynval1val2...valn芍阎,將所有的keys放置到一起枯怖,再將所有的values放置到一起,相對(duì)于以交替方式存放key能曾、value:key1/val1/key2/val2/.../.../keyn/valn/要復(fù)雜的多度硝;不過通過這種方式能夠在key和value長度不同時(shí)肿轨,能夠節(jié)省padding的空間,比如定義map[int64]int8蕊程,相鄰的4個(gè)int8能夠存放到一個(gè)內(nèi)存單元椒袍,若是使用key、value交替則會(huì)導(dǎo)致每個(gè)int8會(huì)被padding占用單獨(dú)的內(nèi)存單元.


hmap結(jié)構(gòu)

整個(gè)hmap不僅包含一個(gè)tophash藻茂,還包括8個(gè)鍵值對(duì)和一個(gè)overflow指針驹暑,這樣使得overflow以鏈表的結(jié)構(gòu)出現(xiàn),一般都是通過指針來訪問鍵值對(duì)和overflow的內(nèi)容辨赐。

mapextra結(jié)構(gòu)源碼:主要用于當(dāng)bukcets元素超過8個(gè)鍵值對(duì)時(shí)优俘,通過鏈表的方式來解決對(duì)應(yīng)的內(nèi)容放置。其包含了map上不存在的fields掀序。

type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap

    nextOverflow *bmap//指向下一個(gè)overflow的bucket
}

詳見:runtime/map.go源碼

三帆焕、實(shí)例

測(cè)試代碼如下:
var intMap map[int]int
var cnt = 8192

func main() {
    printMemStats() //打印出memory情況

    initMap()  // 創(chuàng)建map
    runtime.GC()  // 強(qiáng)制執(zhí)行GC
    printMemStats() // 在強(qiáng)制GC之后 再打印memory情況

    log.Println(len(intMap))  // 查看map的元素個(gè)數(shù)
    for i := 0; i < cnt; i++ {
        delete(intMap, i) // 執(zhí)行delete的操作
    }
    log.Println(len(intMap)) // 驗(yàn)證執(zhí)行delete操作對(duì)實(shí)際map的元素個(gè)數(shù)影響

    runtime.GC()  // 強(qiáng)制執(zhí)行GC
    printMemStats()

    intMap = nil  // 將map置為nil 釋放其占用的內(nèi)存空間
    runtime.GC()
    printMemStats()
}

func initMap() {
    intMap = make(map[int]int, cnt)

    for i := 0; i < cnt; i++ {
        intMap[i] = i
    }
}

func printMemStats() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    log.Printf("Alloc = %v TotalAlloc = %v Sys = %v NumGC = %v\n", m.Alloc/1024, m.TotalAlloc/1024, m.Sys/1024, m.NumGC)
}
輸出結(jié)果
2019/03/19 10:17:17 Alloc = 128 TotalAlloc = 128 Sys = 4868 NumGC = 0
2019/03/19 10:17:17 Alloc = 449 TotalAlloc = 500 Sys = 6338 NumGC = 1
2019/03/19 10:17:17 8192
2019/03/19 10:17:17 0
2019/03/19 10:17:17 Alloc = 451 TotalAlloc = 503 Sys = 6402 NumGC = 2
2019/03/19 10:17:17 Alloc = 138 TotalAlloc = 504 Sys = 6402 NumGC = 3

結(jié)論如下:

  • NumGC 是垃圾回收次數(shù);Alloc 是對(duì)對(duì)象大小不恭,單位是 KB叶雹;Sys 是從 OS 獲取的內(nèi)存大小,單位是 KB换吧;
  • 第一行折晦,沒有進(jìn)行過 GC,默認(rèn)真用了 100 KB 的內(nèi)存沾瓦;
  • map初始化完成之后進(jìn)行一次 GC满着,此時(shí)內(nèi)存占了 422 KB;
  • 接下來就是執(zhí)行delete操作贯莺,可以看到map已經(jīng)被清空了漓滔,也執(zhí)行了一次 GC,但是內(nèi)存沒有被釋放乖篷;
  • 最后把map置為空,內(nèi)存才被釋放透且。

四撕蔼、優(yōu)化

1、刪除

delete對(duì)應(yīng)的源碼

從源碼中可以看到:外層的循環(huán)就是在遍歷整個(gè) map秽誊,刪除的核心就在那個(gè)empty鲸沮。它修改了當(dāng)前 key 的標(biāo)記,而不是直接刪除了內(nèi)存里面的數(shù)據(jù)【只是標(biāo)記為empty锅论,并沒有真正刪除其對(duì)應(yīng)的數(shù)據(jù)

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
        ...省略代碼
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            b.tophash[i] = empty
            h.count--
        }
    }
     ...省略代碼
}

需要注意:若是用map做緩存讼溺,而每次更新只是部分更新,更新的 key 如果偏差比較大最易,有可能會(huì)有內(nèi)存逐漸增長而不釋放的問題怒坯。
不過很多人可能很奇怪為嘛要以這種方式來設(shè)計(jì)map的刪除操作呢:在遍歷map的時(shí)候刪除里面的元素炫狱,頁可以刪除沒有遍歷到的元素,那么為了保證刪除了之后遍歷不發(fā)生異常剔猿,是不能將對(duì)應(yīng)位置空間釋放掉會(huì)觸發(fā)panic视译。那么這算不算內(nèi)存泄漏呢?
若是后續(xù)繼續(xù)對(duì)當(dāng)前的map進(jìn)行write操作归敬,寫入的值剛好命中前面已被“刪除”的bucket酷含,則會(huì)將當(dāng)面bucket的empty內(nèi)容進(jìn)行覆蓋。在這一點(diǎn)上是不能算內(nèi)存泄漏的汪茧。
在實(shí)際一些高性能椅亚、高并發(fā)的場景下,使用map來用來內(nèi)存存儲(chǔ)可能會(huì)帶來一些挑戰(zhàn)舱污,我們可能使用如下的方式來進(jìn)行map的優(yōu)化:

  • 1呀舔、去除無用、重復(fù)的存儲(chǔ)使用
  • 2、盡量少用map或map的value盡量少為指針奔脐,太多的指針類型會(huì)造成GC掃描時(shí)間的增加铜幽;
  • 3、若是用map用作緩存存儲(chǔ)省古,當(dāng)每次只更新部分,更新的key若是偏差較大丧失,會(huì)有可能造成內(nèi)存逐漸增長而不釋放的問題豺妓,可以通過定時(shí)拷貝map的方式來解決,數(shù)十萬的int64內(nèi)存拷貝<100ms的布讹。_
  • 4琳拭、釋放map所占的內(nèi)存 則通過map=nil

2、查詢

訪問map中key源碼
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
        ...省略部分源碼
    // do some race detect things
    // do some memory sanitizer thins
 
    if h == nil || h.count == 0 {
        return unsafe.Pointer(&zeroVal[0])
    }
    if h.flags&hashWriting != 0 {  // 檢測(cè)是否并發(fā)寫描验,map不是gorountine安全的
        throw("concurrent map read and map write")
    }
    alg := t.key.alg  // 哈希算法 alg -> algorithm
    hash := alg.hash(key, uintptr(h.hash0))
    m := bucketMask(h.B)
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
        // 如果老的bucket沒有被移動(dòng)完白嘁,那么去老的bucket中尋找
    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
        }
    }
        // 尋找過程:不斷比對(duì)tophash和key
    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])
}

常見問題

Q:刪除掉map中的元素是否會(huì)釋放內(nèi)存?
A:不會(huì)膘流,刪除操作僅僅將對(duì)應(yīng)的tophash[i]設(shè)置為empty絮缅,并非釋放內(nèi)存。若要釋放內(nèi)存只能等待指針無引用后被系統(tǒng)gc

Q:如何并發(fā)地使用map呼股?
A:map不是goroutine安全的耕魄,所以在有多個(gè)gorountine對(duì)map進(jìn)行寫操作是會(huì)panic。多gorountine讀寫map是應(yīng)加鎖(RWMutex)彭谁,或使用sync.Map(不過不太推薦使用)

Q:map的iterator是否安全吸奴?
A:map的delete并非真的delete,所以對(duì)迭代器是沒有影響的,是安全的则奥。

參考文章

golang map源碼詳解
Golang map 如何進(jìn)行刪除操作考润?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市逞度,隨后出現(xiàn)的幾起案子额划,更是在濱河造成了極大的恐慌,老刑警劉巖档泽,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俊戳,死亡現(xiàn)場離奇詭異,居然都是意外死亡馆匿,警方通過查閱死者的電腦和手機(jī)抑胎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渐北,“玉大人阿逃,你說我怎么就攤上這事≡咧耄” “怎么了恃锉?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呕臂。 經(jīng)常有香客問我破托,道長,這世上最難降的妖魔是什么歧蒋? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任土砂,我火速辦了婚禮,結(jié)果婚禮上谜洽,老公的妹妹穿的比我還像新娘萝映。我一直安慰自己,他們只是感情好阐虚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布序臂。 她就那樣靜靜地躺著,像睡著了一般实束。 火紅的嫁衣襯著肌膚如雪奥秆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天磕洪,我揣著相機(jī)與錄音,去河邊找鬼诫龙。 笑死析显,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的签赃。 我是一名探鬼主播谷异,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼分尸,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了歹嘹?” 一聲冷哼從身側(cè)響起箩绍,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尺上,沒想到半個(gè)月后材蛛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怎抛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年卑吭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片马绝。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡豆赏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出富稻,到底是詐尸還是另有隱情掷邦,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布椭赋,位于F島的核電站抚岗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏纹份。R本人自食惡果不足惜苟跪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蔓涧。 院中可真熱鬧件已,春花似錦、人聲如沸元暴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茉盏。三九已至鉴未,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸠姨,已是汗流浹背铜秆。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留讶迁,地道東北人连茧。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親啸驯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子客扎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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