Golang map 的底層實現(xiàn)

在開發(fā)過程中璧眠,map是必不可少的數(shù)據(jù)結(jié)構(gòu),在Golang中迈螟,使用map或多或少會遇到與其他語言不一樣的體驗海渊,比如訪問不存在的元素會返回其類型的空值、map的大小究竟是多少勤婚,為什么會報"cannot take the address of"錯誤摹量,遍歷map的隨機性等等。
本文希望通過研究map的底層實現(xiàn),以解答這些疑惑缨称。
基于Golang 1.8.3

1. 數(shù)據(jù)結(jié)構(gòu)及內(nèi)存管理

hashmap的定義位于 src/runtime/hashmap.go 中凝果,首先我們看下hashmap和bucket的定義:

type hmap struct {
    count     int    // 元素的個數(shù)
    flags     uint8  // 狀態(tài)標志
    B         uint8  // 可以最多容納 6.5 * 2 ^ B 個元素,6.5為裝載因子
    noverflow uint16 // 溢出的個數(shù)
    hash0     uint32 // 哈希種子

    buckets    unsafe.Pointer // 桶的地址
    oldbuckets unsafe.Pointer // 舊桶的地址睦尽,用于擴容
    nevacuate  uintptr        // 搬遷進度器净,小于nevacuate的已經(jīng)搬遷
    overflow *[2]*[]*bmap 
}

其中,overflow是一個指針当凡,指向一個元素個數(shù)為2的數(shù)組山害,數(shù)組的類型是一個指針,指向一個slice沿量,slice的元素是桶(bmap)的地址浪慌,這些桶都是溢出桶;為什么有兩個朴则?因為Go map在hash沖突過多時权纤,會發(fā)生擴容操作,為了不全量搬遷數(shù)據(jù)乌妒,使用了增量搬遷汹想,[0]表示當前使用的溢出桶集合,[1]是在發(fā)生擴容時撤蚊,保存了舊的溢出桶集合古掏;overflow存在的意義在于防止溢出桶被gc。

// A bucket for a Go map.
type bmap struct {
    // 每個元素hash值的高8位拴魄,如果tophash[0] < minTopHash冗茸,表示這個桶的搬遷狀態(tài)
    tophash [bucketCnt]uint8
    // 接下來是8個key席镀、8個value匹中,但是我們不能直接看到;為了優(yōu)化對齊豪诲,go采用了key放在一起顶捷,value放在一起的存儲方式,
    // 再接下來是hash沖突發(fā)生時屎篱,下一個溢出桶的地址
}

tophash的存在是為了快速試錯服赎,畢竟只有8位,比較起來會快一點交播。

從定義可以看出重虑,不同于STL中map以紅黑樹實現(xiàn)的方式,Golang采用了HashTable的實現(xiàn)秦士,解決沖突采用的是鏈地址法缺厉。也就是說,使用數(shù)組+鏈表來實現(xiàn)map。特別的提针,對于一個key命爬,幾個比較重要的計算公式為:

key hash hashtop bucket index
key hash := alg.hash(key, uintptr(h.hash0)) top := uint8(hash >> (sys.PtrSize*8 - 8)) bucket := hash & (uintptr(1)<<h.B - 1),即 hash % 2^B

例如辐脖,對于B = 3饲宛,當hash(key) = 4時, hashtop = 0嗜价, bucket = 4艇抠,當hash(key) = 20時,hashtop = 0久锥, bucket = 4练链;這個例子我們在搬遷過程還會用到。

內(nèi)存布局類似于這樣:


hashmap-buckets

2. 創(chuàng)建 - makemap

map的創(chuàng)建比較簡單奴拦,在參數(shù)校驗之后媒鼓,需要找到合適的B來申請桶的內(nèi)存空間,接著便是穿件hmap這個結(jié)構(gòu)错妖,以及對它的初始化绿鸣。

makemap

3. 訪問 - mapaccess

對于給定的一個key,可以通過下面的操作找到它是否存在


image.png

方法定義為

// returns key, if not find, returns nil
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer 

// returns key and exist. if not find, returns nil, false
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)

// returns both key and value. if not find, returns nil, nil
func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer)

可見在找不到對應(yīng)key的情況下暂氯,會返回nil

4. 分配 - mapassign

為一個key分配空間的邏輯潮模,大致與查找類似;但增加了寫保護和擴容的操作痴施;注意擎厢,分配過程和刪除過程都沒有在oldbuckets中查找,這是因為首先要進行擴容判斷和操作辣吃;如下:


assign

擴容是整個hashmap的核心算法动遭,我們放在第6部分重點研究。

新建一個溢出桶神得,并將其拼接在當前桶的尾部厘惦,實現(xiàn)了類似鏈表的操作:

// 獲取當前桶的溢出桶
func (b *bmap) overflow(t *maptype) *bmap {
    return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize))
}

// 設(shè)置當前桶的溢出桶
func (h *hmap) setoverflow(t *maptype, b, ovf *bmap) {
    h.incrnoverflow()
    if t.bucket.kind&kindNoPointers != 0 {
        h.createOverflow()
        //重點,這里講溢出桶append到overflow[0]的后面
        *h.overflow[0] = append(*h.overflow[0], ovf)
    }
    *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) = ovf
}

5. 刪除 - mapdelete

刪除某個key的操作與分配類似哩簿,由于hashmap的存儲結(jié)構(gòu)是數(shù)組+鏈表宵蕉,所以真正刪除key僅僅是將對應(yīng)的slot設(shè)置為empty,并沒有減少內(nèi)存节榜;如下:


mapdelete

6. 擴容 - growWork

首先羡玛,判斷是否需要擴容的邏輯是

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

何時h.oldbuckets不為nil呢?在分配assign邏輯中宗苍,當沒有位置給key使用稼稿,而且滿足測試條件(裝載因子>6.5或有太多溢出通)時亿遂,會觸發(fā)hashGrow邏輯:

func hashGrow(t *maptype, h *hmap) {
    //判斷是否需要sameSizeGrow,否則"真"擴
    bigger := uint8(1)
    if !overLoadFactor(int64(h.count), h.B) {
        bigger = 0
        h.flags |= sameSizeGrow
    }
        // 下面將buckets復(fù)制給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
        // 設(shè)置溢出桶
    if h.overflow != nil {
        if h.overflow[1] != nil {
            throw("overflow is not nil")
        }
// 交換溢出桶
        h.overflow[1] = h.overflow[0]
        h.overflow[0] = nil
    }
}

OK渺杉,下面正式進入重點蛇数,擴容階段;在assign和delete操作中是越,都會觸發(fā)擴容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)
    }
}

6.1 搬遷過程

一般來說,新桶數(shù)組大小是原來的2倍(在!sameSizeGrow()條件下)倚评,新桶數(shù)組前半段可以"類比"為舊桶浦徊,對于一個key,搬遷后落入哪一個索引中呢天梧?

假設(shè)舊桶數(shù)組大小為2^B盔性, 新桶數(shù)組大小為2*2^B,對于某個hash值X
若 X & (2^B) == 0呢岗,說明 X < 2^B冕香,那么它將落入與舊桶集合相同的索引xi中;
否則后豫,它將落入xi + 2^B中悉尾。

例如,對于舊B = 3時挫酿,hash1 = 4构眯,hash2 = 20,其搬遷結(jié)果類似這樣早龟。

example.png

源碼中有些變量的命名比較簡單惫霸,容易擾亂思路,我們注明一下便于理解葱弟。

變量 釋義
x *bmap 桶x表示與在舊桶時相同的位置壹店,即位于新桶前半段
y *bmap 桶y表示與在舊桶時相同的位置+舊桶數(shù)組大小,即位于新桶后半段
xi int 桶x的slot索引
yi int 桶y的slot索引
xk unsafe.Pointer 索引xi對應(yīng)的key地址
yk unsafe.Pointer 索引yi對應(yīng)的key地址
xv unsafe.Pointer 索引xi對應(yīng)的value地址
yv unsafe.Pointer 索引yi對應(yīng)的value地址

搬遷過程如下:


evacuate

總結(jié)

到目前為止翘悉,Golang的map實現(xiàn)細節(jié)已經(jīng)分析完畢茫打,但不包含迭代器相關(guān)操作居触。通過分析妖混,我們了解了map是由數(shù)組+鏈表實現(xiàn)的HashTable,其大小和B息息相關(guān)轮洋,同時也了解了map的創(chuàng)建制市、查詢、分配弊予、刪除以及擴容搬遷原理祥楣。總的來說,Golang通過hashtop快速試錯加快了查找過程误褪,利用空間換時間的思想解決了擴容的問題责鳍,利用將8個key(8個value)依次放置減少了padding空間等等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兽间,一起剝皮案震驚了整個濱河市历葛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嘀略,老刑警劉巖恤溶,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異帜羊,居然都是意外死亡咒程,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門讼育,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帐姻,“玉大人,你說我怎么就攤上這事奶段÷舫瑁” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵忧饭,是天一觀的道長扛伍。 經(jīng)常有香客問我,道長词裤,這世上最難降的妖魔是什么刺洒? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮吼砂,結(jié)果婚禮上逆航,老公的妹妹穿的比我還像新娘。我一直安慰自己渔肩,他們只是感情好因俐,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著周偎,像睡著了一般抹剩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蓉坎,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天澳眷,我揣著相機與錄音,去河邊找鬼蛉艾。 笑死钳踊,一個胖子當著我的面吹牛衷敌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拓瞪,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼缴罗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祭埂?” 一聲冷哼從身側(cè)響起瞒爬,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沟堡,沒想到半個月后侧但,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡航罗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年禀横,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粥血。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡柏锄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出复亏,到底是詐尸還是另有隱情趾娃,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布缔御,位于F島的核電站抬闷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏耕突。R本人自食惡果不足惜笤成,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望眷茁。 院中可真熱鬧炕泳,春花似錦、人聲如沸上祈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽登刺。三九已至籽腕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間塘砸,已是汗流浹背节仿。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掉蔬,地道東北人廊宪。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像女轿,于是被迫代替她去往敵國和親箭启。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345