12 Go語言map底層淺析

Go語言map底層淺析

[TOC]git

籠統的來講,go的map底層是一個hash表,經過鍵值對進行映射。 鍵經過哈希函數生成哈希值,而後go底層的map數據結構就存儲相應的hash值,進行索引,最終是在底層使用的數組存儲key,和value。稍微詳細的說,就設計到go map 的結構。hmap 和bmap。github

一、Hash函數

哈希表就不得不說hash函數。hash函數,有加密型和非加密型。加密型的通常用於加密數據、數字摘要等,典型表明就是md五、sha一、sha25六、aes256這種;非加密型的通常就是查找。在map的應用場景中,用的是查找。選擇hash函數主要考察的是兩點:性能、碰撞機率。golang使用的hash算法根據硬件選擇,若是cpu支持aes,那麼使用aes hash,不然使用memhash,memhash是參考xxhash、cityhash實現的,性能很是好。golang

具體hash函數的性能比較能夠看:http://aras-p.info/blog/2016/...算法

哈希函數會將傳入的key值進行哈希運算,獲得一個惟一的值。go語言把生成的哈希值一分爲二,好比一個key通過哈希函數,生成的哈希值爲:8423452987653321,go語言會這它拆分爲84234529,和87653321。那麼,前半部分就叫作高位哈希值,後半部分就叫作低位哈希值。後面會說高位哈希值和低位哈希值是作什麼用的。數組

高位哈希值:是用來肯定當前的bucket(桶)有沒有所存儲的數據的。數據結構

低位哈希值:是用來肯定,當前的數據存在了哪一個bucket(桶)函數

二、map 源碼

2.1hmap(a header of map)

// Go map的一個header結構
type hmap struct {
    count     int // map的大小.  len()函數就取的這個值
    flags     uint8 //map狀態標識
    B         uint8  // 能夠最多容納 6.5 * 2 ^ B 個元素,6.5爲裝載因子即:map長度=6.5*2^B
                    //B能夠理解爲buckets已擴容的次數
    noverflow uint16 // 溢出buckets的數量
    hash0     uint32 // hash 種子

    buckets    unsafe.Pointer //指向最大2^B個Buckets數組的指針. count==0時爲nil.
    oldbuckets unsafe.Pointer //指向擴容以前的buckets數組,而且容量是如今一半.不增加就爲nil
    nevacuate  uintptr  // 搬遷進度,小於nevacuate的已經搬遷
    extra *mapextra // 可選字段,額外信息
}

hmap是map的最外層的一個數據結構,包括了map的各類基礎信息、如大小、bucket。首先說一下,buckets這個參數,它存儲的是指向buckets數組的一個指針,當bucket(桶爲0時)爲nil。咱們能夠理解爲,hmap指向了一個空bucket數組,而且當bucket數組須要擴容時,它會開闢一倍的內存空間,而且會漸進式的把原數組拷貝,即用到舊數組的時候就拷貝到新數組。性能

2.2 bmap(a bucket of map)

// Go map 的 buckets結構
type bmap struct {
    // 每一個元素hash值的高8位,若是tophash[0] < minTopHash,表示這個桶的搬遷狀態
    tophash [bucketCnt]uint8
  // 第二個是8個key、8個value,可是咱們不能直接看到;爲了優化對齊,go採用了key放在一塊兒,value放在一塊兒的存儲方式,
   // 第三個是溢出時,下一個溢出桶的地址
}

bucket(桶),每個bucket最多放8個key和value,最後由一個overflow字段指向下一個bmap,注意key、value、overflow字段都不顯示定義,而是經過maptype計算偏移獲取的。測試

在這裏插入圖片描述

bucket這三部份內容決定了它是怎麼工做的:優化

  • 它的tophash 存儲的是哈希函數算出的哈希值的高八位。是用來加快索引的。由於把高八位存儲起來,這樣不用完整比較key就能過濾掉不符合的key,加快查詢速度當一個哈希值的高8位和存儲的高8位相符合,再去比較完整的key值,進而取出value。
  • 第二部分,存儲的是key 和value,就是咱們傳入的key和value,注意,它的底層排列方式是,key所有放在一塊兒,value所有放在一塊兒。當key大於128字節時,bucket的key字段存儲的會是指針,指向key的實際內容;value也是同樣。
  • 這樣排列好處是在key和value的長度不一樣的時候,能夠消除padding帶來的空間浪費。而且每一個bucket最多存放8個鍵值對
  • 第三部分,存儲的是當bucket溢出時,指向的下一個bucket的指針

    key和value排列

bucket的設計細節:

在golang map中出現衝突時,不是每個key都申請一個結構經過鏈表串起來,而是以bmap爲最小粒度掛載,一個bmap能夠放8個key和value。這樣減小對象數量,減輕管理內存的負擔,利於gc。
若是插入時,bmap中key超過8,那麼就會申請一個新的bmap(overflow bucket)掛在這個bmap的後面造成鏈表,優先用預分配的overflow bucket,若是預分配的用完了,那麼就malloc一個掛上去。注意golang的map不會shrink,內存只會越用越多,overflow bucket中的key全刪了也不會釋放

三、hmap和bmap結構圖

在這裏插入圖片描述

如圖所示:

  • hmap存儲了一個指向底層bucket數組的指針。
  • 咱們存入的key和value是存儲在bucket裏面中,若是key和value大於128字節,那麼bucket裏面存儲的是指向咱們key和value的指針,若是不是則存儲的是值。
  • 每一個bucket 存儲8個key和value,若是超過就從新建立一個bucket掛在在元bucket上,持續掛接造成鏈表。
  • 高位哈希值:是用來肯定當前的bucket(桶)有沒有所存儲的數據的。
  • 低位哈希值:是用來肯定,當前的數據存在了哪一個bucket(桶)

    簡單結構爲圖:

    在這裏插入圖片描述

    工做流程

    查找或者操做map時,首先key通過hash函數生成hash值,經過哈希值的低8位來判斷當前數據屬於哪一個桶(bucket),找到bucket之後,經過哈希值的高八位與bucket存儲的高位哈希值循環比對,若是相同就比較剛纔找到的底層數組的key值,若是key相同,取出value。若是高八位hash值在此bucket沒有,或者有,可是key不相同,就去鏈表中下一個溢出bucket中查找,直到查找到鏈表的末尾。

    碰撞衝突:若是不一樣的key定位到了統一bucket或者生成了同一hash,就產生衝突。 go是經過鏈表法來解決衝突的。好比一個高八位的hash值和已經存入的hash值相同,而且此bucket存的8個鍵值對已經滿了,或者後面已經掛了好幾個bucket了。那麼這時候要存這個值就先比對key,key確定不相同啊,那就今後位置一直沿着鏈表日後找,找到一個空位置,存入它。因此這種狀況,兩個相同的hash值高8位是存在不一樣bucket中的。

    查的時候也是比對hash值和key 沿着鏈表把它查出來。 還有一種狀況,就是目前就 1個bucket,而且8個key-value的數組尚未存滿,這個時候再比較完key不相同的時候,一樣是沿着當前bucket數組中的內存空間日後找,找到第一個空位,插入它。這個就至關因而用尋址法來解決衝突,查找的時候,也是先比較hash值,再比較key,而後沿着當前內存地址日後找。

    go語言的map經過數組+鏈表的方式實現了hash表,同時分散各個桶,使用鏈表法+bucket內部的尋址法解決了碰撞衝突,也提升了效率。由於即便鏈表很長了,go會根據裝載因子,去擴容整個bucket數組,因此下面就要看下擴容。

四、map的擴容

  • 當鏈表愈來愈長,bucket的擴容次數達到必定值,實際上是bmap擴容的加載因數達到6.5,bmap就會進行擴容,將原來bucket數組數量擴充一倍,產生一個新的bucket數組,也就是bmap的buckets屬性指向的數組。這樣bmap中的oldbuckets屬性指向的就是舊bucket數組。
  • 這裏的加載因子LoadFactor是一個閾值,計算方式爲(map長度/2^B )若是超過6.5,將會進行擴容,這個是通過測試才得出的合理的一個閾值。由於,加載因子越小,空間利用率就小,加載因子越大,產生衝突的概率就大。因此6.5是一個平衡的值。
  • map的擴容不會立馬所有複製,而是漸進式擴容,即首先開闢2倍的內存空間,建立一個新的bucket數組。只有當訪問原來就的bucket數組時,纔會將就得bucket拷貝到新的bucket數組,進行漸進式的擴容。固然舊的數據不會刪除,而是去掉引用,等待gc回收。

五、結語

這篇文章,只是對map底層的結構進行了說明,具體建立、查找、刪除等的流程是差很少,可是具體的細節仍是有不少。因此,我就不一一寫出了,貼一下其餘博主寫的文章,很詳細。
5.1 剖析golang map的實現
​ 地址:https://www.jianshu.com/p/092...
5.2 Golang map 的底層實現
​ 地址:https://www.jianshu.com/p/aa0...

重點來了,必須看:很是詳細的map源碼說明

https://github.com/cch123/golang-notes/blob/master/map.md

參考:

https://www.jianshu.com/p/092...

https://www.jianshu.com/p/aa0...

https://blog.csdn.net/i644803...

相關文章
相關標籤/搜索