golang - map

1. 底層原理
hmap

Go中的map是一個(gè)指針,占用8個(gè)字節(jié)闭翩,指向底層的hmap結(jié)構(gòu)體(hash表),在源碼包src/runtime/map.go中定義了該結(jié)構(gòu)體,如下所示:

// A header for a Go map.
type hmap struct {
    // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
    // Make sure this stays in sync with the compiler's definition.
    count     int // # live cells == size of map.  Must be first (used by len() builtin)   代表哈希表中的元素個(gè)數(shù)迄埃,調(diào)用len(map)時(shí)疗韵,返回的就是該字段值
    flags     uint8  //  狀態(tài)標(biāo)志(是否處于正在寫(xiě)入的狀態(tài)等)
    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)      buckets(桶)的對(duì), 如果B=5,則buckets數(shù)組的長(zhǎng)度 = 2^B=32侄非,意味著有32個(gè)桶
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details,      溢出桶的數(shù)量
    hash0     uint32 // hash seed      生成hash的隨機(jī)數(shù)種子

    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.       指向buckets數(shù)組的指針蕉汪,數(shù)組大小為2^B,如果元素個(gè)數(shù)為0逞怨,它為nil者疤。
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing  .如果發(fā)生擴(kuò)容,oldbuckets是指向老的buckets數(shù)組的指針叠赦,老的buckets數(shù)組大小是新的buckets的1/2;非擴(kuò)容狀態(tài)下驹马,它為nil。
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)     表示擴(kuò)容進(jìn)度眯搭,小于此地址的buckets代表已搬遷完成

    extra *mapextra // optional fields
}
map 結(jié)構(gòu)
bmap

上面說(shuō)到hmap 中有一個(gè)buckets 數(shù)組窥翩,其中數(shù)組中的每一個(gè)元素稱為bucket(桶) ,我們也叫作 bmap鳞仙。
一個(gè)桶里面會(huì)最多裝 8 個(gè) 鍵值對(duì)寇蚊,這些 鍵值對(duì) 之所以會(huì)落入同一個(gè)桶,是因?yàn)樗鼈兊膋ey經(jīng)過(guò)哈希計(jì)算后棍好,哈希結(jié)果的低B位(hmap 中的B字段)是相同的仗岸,關(guān)于key的定位我們?cè)趍ap的查詢中詳細(xì)說(shuō)明。在桶內(nèi)借笙,又會(huì)根據(jù) key 計(jì)算出來(lái)的 hash 值的高 8 位來(lái)決定 key 到底落入桶內(nèi)的哪個(gè)位置(一個(gè)桶內(nèi)最多有8個(gè)位置)扒怖。 下面是bmap 結(jié)構(gòu)體的定義,其中tophash是一個(gè)長(zhǎng)度為8的數(shù)組(bucketCnt = 1 << bucketCntBits业稼,bucketCntBits=3)盗痒,用來(lái)快速定位key,如果key的tophash 值在這個(gè)數(shù)組中低散,則代表該key在該桶中

// A bucket for a Go map.
type bmap struct {
    // tophash generally contains the top byte of the hash value
    // for each key in this bucket. If tophash[0] < minTopHash,
    // tophash[0] is a bucket evacuation state instead.
    tophash [bucketCnt]uint8
    // Followed by bucketCnt keys and then bucketCnt elems.
    // NOTE: packing all the keys together and then all the elems together makes the
    // code a bit more complicated than alternating key/elem/key/elem/... but it allows
    // us to eliminate padding which would be needed for, e.g., map[int64]int8.
    // Followed by an overflow pointer.
}

上面bmap結(jié)構(gòu)是靜態(tài)結(jié)構(gòu)俯邓,在編譯過(guò)程中runtime.bmap會(huì)拓展成以下結(jié)構(gòu)體:

type bmap struct{
    tophash [8]uint8
    keys [8]keytype 
    // keytype 由編譯器編譯時(shí)候確定
    values [8]elemtype 
    // elemtype 由編譯器編譯時(shí)候確定
    overflow uintptr 
    // overflow指向下一個(gè)bmap,overflow是uintptr而不是*bmap類型熔号,保證bmap完全不含指針稽鞭,是為了減少gc,溢出桶存儲(chǔ)到extra字段中
}

tophash就是用于實(shí)現(xiàn)快速定位key的位置引镊,在實(shí)現(xiàn)過(guò)程中會(huì)使用key的hash值的高8位作為tophash值朦蕴,存放在bmap的tophash字段中. 同時(shí)還會(huì)存儲(chǔ)一些狀態(tài)值篮条, 表明當(dāng)前的桶單元的狀態(tài),這些狀態(tài)值都小于minTopHash吩抓。為了避免key哈希值的高8位值和這些狀態(tài)值相等涉茧,產(chǎn)生混淆情況,所以當(dāng)key哈希值高8位若小于minTopHash時(shí)候琴拧,自動(dòng)將其值加上minTopHash作為該key的tophash降瞳。桶單元的狀態(tài)值如下:

emptyRest = 0 // 表明此桶單元為空,且更高索引的單元也是空
emptyOne = 1 // 表明此桶單元為空
evacuatedX = 2 // 用于表示擴(kuò)容遷移到新桶前半段區(qū)間
evacuatedY = 3 // 用于表示擴(kuò)容遷移到新桶后半段區(qū)間
evacuatedEmpty = 4 // 用于表示此單元已遷移
minTopHash = 5 // key的tophash值與桶狀態(tài)值分割線值蚓胸,小于此值的一定代表著桶單元的狀態(tài)挣饥,大于此值的一定是key對(duì)應(yīng)的tophash值

如下的tophash函數(shù), 就是來(lái)計(jì)算key的tophash 值沛膳,可以看到扔枫, 如果小于minTopHash(5), 加上minTopHash作為該key的tophash

// tophash calculates the tophash value for hash.
func tophash(hash uintptr) uint8 {
    top := uint8(hash >> (goarch.PtrSize*8 - 8))
    if top < minTopHash {
        top += minTopHash
    }
    return top
}

bmap內(nèi)存數(shù)據(jù)結(jié)構(gòu)可視化如下:


注意到 key 和 value 是各自放在一起的锹安,并不是 key/value/key/value/... 這樣的形式短荐,當(dāng)key和value類型不一樣的時(shí)候,key和value占用字節(jié)大小不一樣叹哭,使用key/value這種形式可能會(huì)因?yàn)閮?nèi)存對(duì)齊導(dǎo)致內(nèi)存空間浪費(fèi)忍宋,所以Go采用key和value分開(kāi)存儲(chǔ)的設(shè)計(jì),更節(jié)省內(nèi)存空間

map 查找

map的查找流程如下:


map 查找流程
  • 寫(xiě)保護(hù)監(jiān)測(cè):
    函數(shù)首先會(huì)檢查 map 底層hmap標(biāo)志位 flags风罩。如果 flags 的寫(xiě)標(biāo)志位此時(shí)被置 1 了糠排,說(shuō)明有其他協(xié)程在執(zhí)行“寫(xiě)”操作,進(jìn)而導(dǎo)致程序 panic超升,這也說(shuō)明了 map 不是線程安全的入宦。flags標(biāo)識(shí)如下:

// flags
iterator = 1 // there may be an iterator using buckets
oldIterator = 2 // there may be an iterator using oldbuckets
hashWriting = 4 // a goroutine is writing to the map
sameSizeGrow = 8 // the current map growth is to a new map of the same size

// 判斷map 是否是在寫(xiě)的狀態(tài),如果是的話室琢,則拋出異常乾闰,所以map 不是線程安全的
     if h.flags&hashWriting != 0 {
         throw("concurrent map read and map write")
     }
  • 計(jì)算hash:
    計(jì)算map 中的key 的hash 值
hash := t.hasher(key, uintptr(h.hash0))

hasher 函數(shù)類型為 func(unsafe.Pointer, uintptr) uintptr , 將傳入key的指針地址,和hmap的hash 種子
key經(jīng)過(guò)哈希函數(shù)計(jì)算后盈滴,得到的哈希值如下(主流64位機(jī)下共 64 個(gè) bit 位)涯肩, 不同類型的key會(huì)有不同的hash函數(shù)

  • 找到hash對(duì)應(yīng)的bucket:
    bucket定位:key 的hash 值的低B個(gè)bit 位,用來(lái)定位key所存放的bucket
    如果當(dāng)前map正在擴(kuò)容中巢钓,并且定位到的舊bucket數(shù)據(jù)還未完成遷移病苗,則使用舊的bucket(擴(kuò)容前的bucket oldbuckets
// 桶的個(gè)數(shù)m-1,即 1<<B-1,B=5時(shí)竿报,則有0~31號(hào)桶
m := bucketMask(h.B)
// 計(jì)算哈希值對(duì)應(yīng)的bucket
// t.bucketsize為一個(gè)bmap的大小,通過(guò)對(duì)哈希值和桶個(gè)數(shù)取模得到桶編號(hào)继谚,通過(guò)對(duì)桶編號(hào)和buckets起始地址進(jìn)行運(yùn)算烈菌,獲取哈希值對(duì)應(yīng)的bucket
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 是否在擴(kuò)容
if c := h.oldbuckets; c != nil {
  // 桶個(gè)數(shù)已經(jīng)發(fā)生增長(zhǎng)一倍阵幸,則舊bucket的桶個(gè)數(shù)為當(dāng)前桶個(gè)數(shù)的一半
    if !h.sameSizeGrow() {
        // There used to be half as many buckets; mask down one more power of two.
        m >>= 1
    }
    // 計(jì)算哈希值對(duì)應(yīng)的舊bucket
    oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
    // 如果舊bucket的數(shù)據(jù)沒(méi)有完成遷移,則使用舊bucket查找
    if !evacuated(oldb) {
        b = oldb
    }
}
  • 遍歷查找bucket:
    通過(guò)tophash 函數(shù)定位:哈希值的高8個(gè)bit 位芽世,用來(lái)快速判斷key是否已在當(dāng)前bucket中挚赊,如果不在的話,需要去bucket的overflow中查找

示例:
假設(shè)某個(gè)key的hash 值為1001011100001111011011001000111100101010001001011001010101000110,其中hmap的B=5 即 有2^5 =32 個(gè)桶(0-31號(hào)), 如下圖所示:


首先通過(guò)hash值的后B位济瓢, 即后五位找到對(duì)應(yīng)的bucket荠割,此時(shí)為00110=6號(hào)桶, 再講key的hash值的高8位通過(guò)tophash 函數(shù)來(lái)找到對(duì)應(yīng)的桶中的所在的下標(biāo)旺矾,

map key 沖突解決方式

而Go map也采用 鏈地址法解決沖突蔑鹦,具體就是插入key到map中時(shí),當(dāng)key定位的桶填滿8個(gè)元素后(這里的單元就是桶箕宙,不是元素)嚎朽,將會(huì)創(chuàng)建一個(gè)溢出桶(overflow ),并且將溢出桶插入當(dāng)前桶所在鏈表尾部

2. map 遍歷的無(wú)序性

使用 range 多次遍歷 map 時(shí)輸出的 key 和 value 的順序可能不同柬帕。主要原因有2點(diǎn):

  • map在遍歷時(shí)哟忍,并不是從固定的0號(hào)bucket開(kāi)始遍歷的,每次遍歷陷寝,都會(huì)從一個(gè)隨機(jī)值序號(hào)的bucket锅很,再?gòu)钠渲须S機(jī)的cell開(kāi)始遍歷
  • map遍歷時(shí),是按序遍歷bucket凤跑,同時(shí)按序遍歷bucket中和其overflow bucket中的cell爆安。但是map在擴(kuò)容后,會(huì)發(fā)生key的搬遷饶火,這造成原來(lái)落在一個(gè)bucket中的key鹏控,搬遷后,有可能會(huì)落到其他bucket中了肤寝,從這個(gè)角度看当辐,遍歷map的結(jié)果就不可能是按照原來(lái)的順序了

如果想要有序遍歷map,只需對(duì) map中的 key 先排序鲤看,再按照 key 的順序遍歷 map

3. 線程安全性

map默認(rèn)是并發(fā)不安全的缘揪,同時(shí)對(duì)map進(jìn)行并發(fā)讀寫(xiě)時(shí),程序會(huì)通過(guò)map 的寫(xiě)保護(hù)監(jiān)測(cè)機(jī)制來(lái)拋出panic異常
要想實(shí)現(xiàn)并發(fā)安全义桂,有如下兩種方式

  • map + 鎖(sync.RWMutex)
  • 使用Go自帶的 sync.Map找筝, 該map 支持并發(fā)讀寫(xiě)安全, sync.Map采取了 “空間換時(shí)間” 的機(jī)制,冗余了兩個(gè)數(shù)據(jù)結(jié)構(gòu)慷吊,分別是:read 和 dirty
type Map struct {
   mu Mutex
   read atomic.Value // readOnly
   dirty map[interface{}]*entry
   misses int
}

和原始map+RWLock的實(shí)現(xiàn)并發(fā)的方式相比袖裕,減少了加鎖對(duì)性能的影響。它做了一些優(yōu)化:可以無(wú)鎖訪問(wèn)read map溉瓶,而且會(huì)優(yōu)先操作read map急鳄,倘若只操作read map就可以滿足要求谤民,那就不用去操作write map(dirty),所以在某些特定場(chǎng)景中它發(fā)生鎖競(jìng)爭(zhēng)的頻率會(huì)遠(yuǎn)遠(yuǎn)小于map+RWLock的實(shí)現(xiàn)方式

優(yōu)點(diǎn):

適合讀多寫(xiě)少的場(chǎng)景

缺點(diǎn):

寫(xiě)多的場(chǎng)景疾宏,會(huì)導(dǎo)致 read map 緩存失效张足,需要加鎖,沖突變多坎藐,性能急劇下降

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末为牍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子岩馍,更是在濱河造成了極大的恐慌碉咆,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兼雄,死亡現(xiàn)場(chǎng)離奇詭異吟逝,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)赦肋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)块攒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人佃乘,你說(shuō)我怎么就攤上這事囱井。” “怎么了趣避?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵庞呕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我程帕,道長(zhǎng)住练,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任愁拭,我火速辦了婚禮讲逛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘岭埠。我一直安慰自己盏混,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布惜论。 她就那樣靜靜地躺著许赃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪馆类。 梳的紋絲不亂的頭發(fā)上混聊,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音乾巧,去河邊找鬼句喜。 笑死僵闯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的藤滥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼社裆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拙绊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起泳秀,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤标沪,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后嗜傅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體金句,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年吕嘀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了违寞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡偶房,死狀恐怖趁曼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情棕洋,我是刑警寧澤挡闰,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站掰盘,受9級(jí)特大地震影響摄悯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愧捕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一奢驯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晃财,春花似錦叨橱、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至钢猛,卻和暖如春伙菜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背命迈。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工贩绕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留火的,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓淑倾,卻偏偏與公主長(zhǎng)得像馏鹤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子娇哆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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

  • 本文目錄如下湃累,閱讀本文后,將一網(wǎng)打盡下面Golang Map相關(guān)面試題 面試題 map的底層實(shí)現(xiàn)原理 為什么遍歷m...
    Go程序員閱讀 1,143評(píng)論 0 3
  • 由于本文篇幅較長(zhǎng)碍讨,故將目錄整理如下 什么是Map 維基百科的定義 In computer science, an ...
    機(jī)器鈴砍菜刀s閱讀 173評(píng)論 0 0
  • 前言 網(wǎng)上分析golang中map的源碼的博客已經(jīng)非常多了治力,隨便一搜就有,而且也非常詳細(xì)勃黍,所以如果我再來(lái)寫(xiě)就有點(diǎn)畫(huà)...
    LinkinStar閱讀 422評(píng)論 0 2
  • golang map的底層實(shí)現(xiàn) 粗略的講宵统,Go語(yǔ)言中map采用的是哈希查找表,由一個(gè)key通過(guò)哈希函數(shù)得到哈希值覆获,...
    昔召閬夢(mèng)閱讀 1,041評(píng)論 0 1
  • map 的設(shè)計(jì)也被稱為 “The dictionary problem”马澈,它的任務(wù)是設(shè)計(jì)一種數(shù)據(jù)結(jié)構(gòu)用來(lái)維護(hù)一個(gè)集...
    夜空中乄最亮的星閱讀 1,596評(píng)論 0 0