每天一個(gè)知識(shí)點(diǎn):Go 語(yǔ)言 map 詳解

本文會(huì)介紹 Go 語(yǔ)言的哈希的實(shí)現(xiàn)原理是使用時(shí)應(yīng)該了解的注意事項(xiàng),內(nèi)容全部來(lái)自 Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn) 险毁,基于內(nèi)容和自己的理解,做了一些排版上的設(shè)計(jì)便于個(gè)人的學(xué)習(xí)和回顧,有興趣的可以直接閱讀 Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn) 中的內(nèi)容伶跷。

設(shè)計(jì)原理

哈希函數(shù)

  • 理想:哈希函數(shù)能夠?qū)⒉煌I映射到不同的索引上,哈希函數(shù)的輸出范圍大于輸入范圍秘狞。
  • 實(shí)際:讓哈希函數(shù)的結(jié)果能夠盡可能的均勻分布叭莫,然后通過(guò)工程上的手段解決哈希碰撞的問(wèn)題。
  • 影響結(jié)果:果使用結(jié)果分布較為均勻的哈希函數(shù)烁试,那么哈希的增刪改查的時(shí)間復(fù)雜度為 ??(1)雇初;但是如果哈希函數(shù)的結(jié)果分布不均勻,那么所有操作的時(shí)間復(fù)雜度可能會(huì)達(dá)到 O(n)减响。

沖突解決

  • 開(kāi)放尋址法:
    • 核心思想是依次探測(cè)和比較數(shù)組中的元素以判斷目標(biāo)鍵值對(duì)是否存在于哈希表中靖诗。
    • 當(dāng)我們向當(dāng)前哈希表寫入新的數(shù)據(jù)時(shí),如果發(fā)生了沖突支示,就會(huì)將鍵值對(duì)寫入到下一個(gè)索引不為空的位置刊橘,當(dāng)需要查找某個(gè)鍵對(duì)應(yīng)的值時(shí),會(huì)從索引的位置開(kāi)始線性探測(cè)數(shù)組颂鸿,找到目標(biāo)鍵值對(duì)或者空內(nèi)存就意味著這一次查詢操作的結(jié)束促绵。
    • 開(kāi)放尋址法中對(duì)性能影響最大的是裝載因子,它是數(shù)組中元素的數(shù)量與數(shù)組大小的比值嘴纺。
  • 拉鏈法(Go 語(yǔ)言解決哈希碰撞時(shí)選擇的方法):
    • 一般會(huì)使用數(shù)組加上鏈表败晴,不過(guò)一些編程語(yǔ)言會(huì)在拉鏈法的哈希中引入紅黑樹(shù)以優(yōu)化性能,拉鏈法會(huì)使用鏈表數(shù)組作為哈希底層的數(shù)據(jù)結(jié)構(gòu)栽渴。
    • 向當(dāng)前哈希表寫入新的數(shù)據(jù)時(shí)尖坤,會(huì)先經(jīng)過(guò)一個(gè)哈希函數(shù),哈希函數(shù)返回的哈希會(huì)幫助我們選擇一個(gè)桶闲擦,和開(kāi)放地址法一樣慢味,選擇桶的方式是直接對(duì)哈希返回的結(jié)果取模,選擇了桶后就可以遍歷當(dāng)前桶中的鏈表了墅冷,在遍歷鏈表的過(guò)程中會(huì)遇到以下兩種情況
      • 找到鍵相同的鍵值對(duì) — 更新鍵對(duì)應(yīng)的值纯路;
      • 沒(méi)有找到鍵相同的鍵值對(duì) — 在鏈表的末尾追加新的鍵值對(duì);
    • 哈希表讀寫操作的主要開(kāi)銷
      • 計(jì)算哈希
      • 定位桶
      • 遍歷鏈表
    • 裝載因子 = 元素?cái)?shù)量 ÷ 桶數(shù)量
    • 裝載因子越大俺榆,哈希的讀寫性能就越差感昼。在一般情況下使用拉鏈法的哈希表裝載因子都不會(huì)超過(guò) 1,當(dāng)哈希表的裝載因子較大時(shí)會(huì)觸發(fā)哈希的擴(kuò)容罐脊,創(chuàng)建更多的桶來(lái)存儲(chǔ)哈希中的元素定嗓,保證性能不會(huì)出現(xiàn)嚴(yán)重的下降蜕琴。

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

type hmap struct {
    count     int // 表示當(dāng)前哈希表中的元素?cái)?shù)量
    flags     uint8
    B         uint8 // 表示當(dāng)前哈希表持有的 buckets 數(shù)量,但是因?yàn)楣1碇型暗臄?shù)量都 2 的倍數(shù)宵溅,所以該字段會(huì)存儲(chǔ)對(duì)數(shù)凌简,也就是 len(buckets) == 2^B;
    noverflow uint16
    hash0     uint32 // 是哈希的種子恃逻,它能為哈希函數(shù)的結(jié)果引入隨機(jī)性雏搂,這個(gè)值在創(chuàng)建哈希表時(shí)確定,并在調(diào)用哈希函數(shù)時(shí)作為參數(shù)傳入寇损;

    buckets    unsafe.Pointer
    oldbuckets unsafe.Pointer // 是哈希在擴(kuò)容時(shí)用于保存之前 buckets 的字段凸郑,它的大小是當(dāng)前 buckets 的一半;
    nevacuate  uintptr

    extra *mapextra
}

type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}
// hash 表的桶
type bmap struct {
    tophash [bucketCnt]uint8
}
// 編譯期間 hash 表的桶
type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

初始化

字面量

  • 當(dāng)哈希表中的元素?cái)?shù)量少于或者等于 25 個(gè)時(shí)矛市,編譯器會(huì)將字面量初始化的結(jié)構(gòu)體轉(zhuǎn)換成以下的代碼芙沥,將所有的鍵值對(duì)一次加入到哈希表中:

    hash := make(map[string]int, 3)
    hash["1"] = 2
    hash["3"] = 4
    hash["5"] = 6
    
    
  • 一旦哈希表中元素的數(shù)量超過(guò)了 25 個(gè),編譯器會(huì)創(chuàng)建兩個(gè)數(shù)組分別存儲(chǔ)鍵和值浊吏,這些鍵值對(duì)會(huì)通過(guò)如下所示的 for 循環(huán)加入哈希:

    hash := make(map[string]int, 26)
    vstatk := []string{"1", "2", "3", ... 而昨, "26"}
    vstatv := []int{1, 2, 3, ... , 26}
    for i := 0; i < len(vstak); i++ {
        hash[vstatk[i]] = vstatv[i]
    }
    
    

運(yùn)行時(shí)

  • 當(dāng)創(chuàng)建的哈希被分配到棧上并且其容量小于 BUCKETSIZE = 8 時(shí),Go 語(yǔ)言在編譯階段會(huì)使用如下方式快速初始化哈希找田,這也是編譯器對(duì)小容量的哈希做的優(yōu)化:
  • 當(dāng)桶的數(shù)量小于 2^4 時(shí)歌憨,由于數(shù)據(jù)較少、使用溢出桶的可能性較低墩衙,會(huì)省略創(chuàng)建的過(guò)程以減少額外開(kāi)銷务嫡;
  • 當(dāng)桶的數(shù)量多于 2^4 時(shí),會(huì)額外創(chuàng)建 2^(???4) 個(gè)溢出桶底桂;
  • 正常桶和溢出桶在內(nèi)存中的存儲(chǔ)空間是連續(xù)的植袍,只是被 runtime.hmap 中的不同字段引用惧眠,當(dāng)溢出桶數(shù)量較多時(shí)會(huì)通過(guò) runtime.newobject 創(chuàng)建新的溢出桶籽懦。

讀寫操作

訪問(wèn)

  • 賦值語(yǔ)句左側(cè)接受參數(shù)的個(gè)數(shù)會(huì)決定使用的運(yùn)行時(shí)方法:
    • 當(dāng)接受一個(gè)參數(shù)時(shí),會(huì)使用 runtime.mapaccess1氛魁,該函數(shù)僅會(huì)返回一個(gè)指向目標(biāo)值的指針暮顺;
    • 當(dāng)接受兩個(gè)參數(shù)時(shí),會(huì)使用 runtime.mapaccess2秀存,除了返回目標(biāo)值之外捶码,它還會(huì)返回一個(gè)用于表示當(dāng)前鍵對(duì)應(yīng)的值是否存在的 bool 值:
    • 訪問(wèn)的過(guò)程為先通過(guò)哈希表設(shè)置的哈希函數(shù)、種子獲取當(dāng)前鍵對(duì)應(yīng)的哈希或链,再通過(guò) runtime.bucketMaskruntime.add 拿到該鍵值對(duì)所在的桶序號(hào)和哈希高位的 8 位數(shù)字惫恼。
    • 哈希會(huì)依次遍歷正常桶和溢出桶中的數(shù)據(jù),它會(huì)先比較哈希的高 8 位和桶中存儲(chǔ)的 tophash澳盐,后比較傳入的和桶中的值以加速數(shù)據(jù)的讀寫祈纯。
    • 用于選擇桶序號(hào)的是哈希的最低幾位令宿,而用于加速訪問(wèn)的是哈希的高 8 位,這種設(shè)計(jì)能夠減少同一個(gè)桶中有大量相等 tophash 的概率影響性能腕窥。
    • 每一個(gè)桶都是一整片的內(nèi)存空間粒没,當(dāng)發(fā)現(xiàn)桶中的 tophash 與傳入鍵的 tophash 匹配之后,就會(huì)通過(guò)指針和偏移量獲取哈希中存儲(chǔ)的鍵 keys[0] 并與 key 比較簇爆,如果兩者相同就會(huì)獲取目標(biāo)值的指針 values[0] 并返回癞松。

寫入

  • 首先是函數(shù)會(huì)根據(jù)傳入的鍵拿到對(duì)應(yīng)的哈希和桶:
  • 然后通過(guò)遍歷比較桶中存儲(chǔ)的 tophash 和鍵的哈希,如果找到了相同結(jié)果就會(huì)返回目標(biāo)位置的地址入蛆。
  • 如果當(dāng)前桶已經(jīng)滿了响蓉,哈希會(huì)調(diào)用 runtime.hmap.newoverflow 創(chuàng)建新桶或者使用 runtime.hmap 預(yù)先在 noverflow 中創(chuàng)建好的桶來(lái)保存數(shù)據(jù),新創(chuàng)建的桶不僅會(huì)被追加到已有桶的末尾哨毁,還會(huì)增加哈希表的 noverflow 計(jì)數(shù)器厕妖。
  • 如果當(dāng)前鍵值對(duì)在哈希中不存在,哈希會(huì)為新鍵值對(duì)規(guī)劃存儲(chǔ)的內(nèi)存地址挑庶,通過(guò) runtime.typedmemmove 將鍵移動(dòng)到對(duì)應(yīng)的內(nèi)存空間中并返回鍵對(duì)應(yīng)值的地址 val言秸。如果當(dāng)前鍵值對(duì)在哈希中存在,那么就會(huì)直接返回目標(biāo)區(qū)域的內(nèi)存地址迎捺,哈希并不會(huì)在 runtime.mapassign 這個(gè)運(yùn)行時(shí)函數(shù)中將值拷貝到桶中举畸,該函數(shù)只會(huì)返回內(nèi)存地址,真正的賦值操作是在編譯期間插入的凳枝。

擴(kuò)容

  • runtime.mapassign 函數(shù)會(huì)在以下兩種情況發(fā)生時(shí)觸發(fā)哈希的擴(kuò)容:
    • 裝載因子已經(jīng)超過(guò) 6.5抄沮;
    • 哈希使用了太多溢出桶;
  • runtime.mapassign 還需要判斷當(dāng)前哈希是否已經(jīng)處于擴(kuò)容狀態(tài)岖瑰,避免二次擴(kuò)容造成混亂叛买。
  • 如果這次擴(kuò)容是溢出的桶太多導(dǎo)致的,那么這次擴(kuò)容就是等量擴(kuò)容 sameSizeGrow 蹋订,等量擴(kuò)容創(chuàng)建的新桶數(shù)量值和舊桶一樣率挣。
  • 如果這是等量擴(kuò)容,那么舊桶與新桶之間是一對(duì)一的關(guān)系露戒,所以兩個(gè) runtime.evacDst 只會(huì)初始化一個(gè)椒功。而當(dāng)哈希表的容量翻倍時(shí),每個(gè)舊桶的元素會(huì)都分流到新創(chuàng)建的兩個(gè)桶中智什。
  • 總結(jié):哈希在存儲(chǔ)元素過(guò)多時(shí)會(huì)觸發(fā)擴(kuò)容操作动漾,每次都會(huì)將桶的數(shù)量翻倍,擴(kuò)容過(guò)程不是原子的荠锭,而是通過(guò) runtime.growWork 增量觸發(fā)的旱眯,在擴(kuò)容期間訪問(wèn)哈希表時(shí)會(huì)使用舊桶,向哈希表寫入數(shù)據(jù)時(shí)會(huì)觸發(fā)舊桶元素的分流。除了這種正常的擴(kuò)容之外删豺,為了解決大量寫入础爬、刪除造成的內(nèi)存泄漏問(wèn)題,哈希引入了 sameSizeGrow 這一機(jī)制吼鳞,在出現(xiàn)較多溢出桶時(shí)會(huì)整理哈希的內(nèi)存減少空間的占用看蚜。

刪除

  • 需要使用 Go 語(yǔ)言中的 delete 關(guān)鍵字,這個(gè)關(guān)鍵字的唯一作用就是將某一個(gè)鍵對(duì)應(yīng)的元素從哈希表中刪除赔桌,無(wú)論是該鍵對(duì)應(yīng)的值是否存在供炎,這個(gè)內(nèi)建的函數(shù)都不會(huì)返回任何的結(jié)果。
  • delete 關(guān)鍵字在編譯期間經(jīng)過(guò)類型檢查中間代碼生成階段被轉(zhuǎn)換成 runtime.mapdelete 函數(shù)簇中的一員疾党,用于處理刪除邏輯的函數(shù)與哈希表的 runtime.mapassign 幾乎完全相同音诫。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市雪位,隨后出現(xiàn)的幾起案子竭钝,更是在濱河造成了極大的恐慌,老刑警劉巖雹洗,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件香罐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡时肿,警方通過(guò)查閱死者的電腦和手機(jī)庇茫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)螃成,“玉大人旦签,你說(shuō)我怎么就攤上這事〈绾辏” “怎么了宁炫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)氮凝。 經(jīng)常有香客問(wèn)我羔巢,道長(zhǎng),這世上最難降的妖魔是什么覆醇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任朵纷,我火速辦了婚禮,結(jié)果婚禮上永脓,老公的妹妹穿的比我還像新娘。我一直安慰自己鞋仍,他們只是感情好常摧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般落午。 火紅的嫁衣襯著肌膚如雪谎懦。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天溃斋,我揣著相機(jī)與錄音界拦,去河邊找鬼。 笑死梗劫,一個(gè)胖子當(dāng)著我的面吹牛享甸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梳侨,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蛉威,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了走哺?” 一聲冷哼從身側(cè)響起蚯嫌,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丙躏,沒(méi)想到半個(gè)月后择示,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晒旅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年对妄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敢朱。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡剪菱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拴签,到底是詐尸還是另有隱情孝常,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布蚓哩,位于F島的核電站构灸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏岸梨。R本人自食惡果不足惜喜颁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望曹阔。 院中可真熱鬧半开,春花似錦、人聲如沸赃份。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至纠永,卻和暖如春鬓长,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尝江。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工涉波, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炭序。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓啤覆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親少态。 傳聞我的和親對(duì)象是個(gè)殘疾皇子城侧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

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