大話圖解golang map

前言

網上分析golang中map的源碼的博客已經很是多了,隨便一搜就有,並且也很是詳細,因此若是我再來寫就有點多此一舉了(並且我也寫很差,手動滑稽)。可是我仍是要寫,略略略,這篇博客的意義在於能從幾張圖片,而後用我最通俗的文字,讓沒看過源碼的人最快程度上了解golang中map是怎麼樣的。html

固然,由於簡單,因此不完美。有不少地方省略了細節問題,若是你以爲沒看夠,或者原本就想了解詳細狀況的話在文末給出了一些很是不錯的博客,固然有能力仍是本身去閱讀源碼比較靠譜。java

那麼下面我將從這幾個方面來講明,你先記住有下面幾個方向,這樣能夠有一個大體的思路:git

  • 基礎結構:golang中的map是什麼樣子的,是由什麼數據結構組成的?
  • 初始化:初始化以後map是怎麼樣的?
  • get:如何獲取一個元素?
  • put:如何存放一個元素?
  • 擴容:當存放空間不夠的時候擴容是怎麼擴的?

基礎結構

圖解


這個就是golang中map的結構,其實真的不復雜,我省略了其中一些和結構關係不大的字段,就只剩下這些了。github

大話

大話來描述一些要點:golang

  • 最外面是hmap結構體,用buckets存放一些名字叫bmap的桶(數量不定,是2的指數倍)
  • bmap是一種有8個格子的桶(必定只有8個格子),每一個格子存放一對key-value
  • bmap有一個overflow,用於鏈接下一個bmap(溢出桶)
  • hmap還有oldbuckets,用於存放老數據(用於擴容時)
  • mapextra用於存放非指針數據(用於優化存儲和訪問),內部的overflow和oldoverflow實際仍是bmap的數組。

這就是map的結構,而後咱們稍微對比總結一下。數組

咱們常見的map如java中的map是直接拿數組,數組中直接對應出了key-value,而在golang中,作了多加中間一層,buckets;java中若是key的哈希相同會採用鏈表的方式鏈接下去,當達到必定程度會轉換紅黑樹,golang中直接相似鏈表鏈接下去,只不過鏈接下去的是buckets。緩存

源碼一瞥

  • 下面附上源碼中它們的樣子,方便以後你本身閱讀的時候有個印象(注意源碼中的樣子和編譯以後是不一樣的喲,golang會根據map存放的類型不一樣來搞定它們實際的樣子)

那麼看完結構你確定會有疑問?爲何要多一層8個格子的bucket呢?咱們怎麼肯定放在8個格子其中的哪一個呢?帶着問題往下看。
安全

初始化

源碼一瞥

初始化就不須要圖去說明了,由於初始化以後就是產生基礎的一個結構,根據map中存放的類型不一樣。這裏主要說明一下,初始化的代碼放在什麼位置。我也刪除了其中一些代碼,大體看看就好。數據結構

// makehmap_small implements Go map creation for make(map[k]v) and
// make(map[k]v, hint) when hint is known to be at most bucketCnt
// at compile time and the map needs to be allocated on the heap.
func makemap_small() *hmap {
    h := new(hmap)
    h.hash0 = fastrand()
    return h
}

// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
    .....

    // initialize Hmap
    if h == nil {
        h = (*hmap)(newobject(t.hmap))
    }
    h.hash0 = fastrand()

    // find size parameter which will hold the requested # of elements
    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B
    
    ......
    return h
}

 

  

其中須要注意一個點:「B」,還記得剛纔說名字叫bmap的桶數量是不肯定的嗎?這個B必定程度上表示的就是桶的數量,固然不是說B是3桶的數量就是3,而是2的3次方,也就是8;當B爲5,桶的數量就是32;記住這個B,後面會用到它。併發

其實你想嘛,初始化還能幹什麼,最重要的確定就是肯定一開始要有多少個桶,初始的大小仍是很重要的,還有一些別的初始化哈希種子等等,問題不大。咱們的重點仍是要放在存/取上面。

GET

圖解

其實從結構上面來看,咱們已經能夠摸到一些門道了。先本身想一下,要從一個hashmap中獲取一個元素,那麼必定是經過key的哈希值去定位到這個元素,那麼想着這個大體方向,看下面一張流程圖來詳細理解golang中是如何實現的。

大話

下面說明要點:

  • 計算出key的hash
  • 用最後的「B」位來肯定在哪一個桶(「B」就是前面說的那個,B爲4,就有16個桶,0101用十進制表示爲5,因此在5號桶)
  • 根據key的前8位快速肯定是在哪一個格子(額外說明一下,在bmap中存放了每一個key對應的tophash,是key的前8位)
  • 最終仍是須要比對key完整的hash是否匹配,若是匹配則獲取對應value
  • 若是都沒有找到,就去下一個overflow找

總結一下:經過後B位肯定桶,經過前8位肯定格子,循環遍歷連着的全部桶所有找完爲止。
那麼爲何要有這個tophash呢?由於tophash能夠快速肯定key是否正確,你能夠把它理解成一種緩存措施,若是前8位都不對了,後面就沒有必要比較了。

源碼一瞥


其中紅色的字標出的地方說明了上面的關鍵點,最後有關key和value具體的存放方式和取出的定位不作深究,有興趣能夠看最後的參考博客。

PUT

其實當你知道了如何GET,那麼PUT就沒有什麼難度了,由於本質是同樣的。PUT的時候同樣的方式去定位key的位置:

  • 經過key的後「B」位肯定是哪個桶
  • 經過key的前8位快速肯定是否已經存在
  • 最終肯定存放位置,若是8個格子已經滿了,沒地方放了,那麼就從新建立一個bmap做爲溢出桶鏈接在overflow

圖解


這裏主要圖解說明一下,若是新來的key發現前面有一個格子空着(這個狀況是刪除形成的),就會記錄這個位置,當所有掃描完成以後發現本身確實是新來的,那麼就會放前面那個空着的,而不會放最後(我把這個稱爲緊湊原則,儘量保證數據存放緊湊,這樣下次掃描會快)

代碼位置

go/src/runtime/hashmap.go的mapassign函數就是map的put方法,由於代碼很長這裏就很少贅述了。

擴容

這個就是最複雜的地方了,可是呢?Don't worry我這裏仍是會省略其中某些部分,將最重要的地方拎出來。

擴容的方式

  1. 相同容量擴容
  2. 2倍容量擴容
    啥意思呢?第一種出現的狀況是:由於map不斷的put和delete,出現了不少空格,這些空格會致使bmap很長,可是中間有不少空的地方,掃描時間變長。因此第一種擴容實際是一種整理,將數據整理到前面一塊兒。第二種呢:就是真的不夠用了,擴容兩倍。

擴容的條件

裝載因子

若是你看過Java的HashMap實現,就知道有個裝載因子,一樣的在golang中也有,可是不同哦。裝載因子的定義是這個樣子:
loadFactor := count / (2B)
其中count爲map中元素的個數,B就是以前個那個「B」
翻譯一下就是裝載因子 = (map中元素的個數)/(map當前桶的個數)

擴容條件1

裝載因子 > 6.5(這個值是源碼中寫的)
其實意思就是,桶只有那麼幾個,可是元素不少,證實有不少溢出桶的存在(能夠想成鏈表拉的太長了),那麼掃描速度會很慢,就要擴容。

擴容條件2

overflow 的 bucket 數量過多:當 B 小於 15,若是 overflow 的 bucket 數量超過 2B ;當 B >= 15,若是 overflow 的 bucket 數量超過 215
其實意思就是,可能有一個單獨的一條鏈拉的很長,溢出桶太多了,說白了就是,加入的key不巧,後B位都同樣,一直落在同一個桶裏面,這個桶一直放,雖然裝載因子不高,可是掃描速度就很慢。

擴容條件3

當前不能正在擴容

圖解


這張圖表示的就是相同容量的擴容,實際上就是一種整理,將分散的數據集合到一塊兒,提升掃描效率。(上面表示擴容以前,下面表示擴容以後)


這張圖表示的是就是2倍的擴容(上面表示擴容以前,下面表示擴容以後),若是有兩個key後三位分別是001和101,當B=2時,只有4個桶,只看最後兩位,這兩個key後兩位都是01因此在一個桶裏面;擴容以後B=3,就會有8個桶,看後面三位,因而它們就分到了不一樣的桶裏面。

大話

下面說一些擴容時的細節:

  • 擴容不是一次性完成的,還記的咱們hmap一開始有一個oldbuckets嗎?是先將老數據存到這個裏面
  • 每次搬運1到2個bucket,當插入或修改、刪除key觸發
  • 擴容以後確定會影響到get和put,遍歷的時候確定會先從oldbuckets拿,put確定也要考慮是否要放到新產生的桶裏面去

源碼一瞥


擴容的三個條件,看到了嗎?這個地方在mapassign方法中。


這裏能夠看到,註釋也寫的很清楚,若是是加載因子超出了,那麼就2倍擴容,若是不是那麼就是由於太多溢出桶了,sameSizeGrow表示就是相同容量擴容


evacuate是搬運方法,這邊能夠看到,每次搬運是1到2個

evacuate實在是太長了,也很是複雜,可是狀況就是圖上描述的那樣,有興趣的能夠詳細去看,這裏不截圖說明了。

總結和小問題

至此你應該對於golang中的map有一個基本的認識了,你還能夠去看看刪除,你還能夠去看看遍歷等等,相信有了上面的基本認識那麼應該不會難到你。下面有幾個小問題:

  1. 是否線程安全?否,並且併發操做會拋出異常。
  2. 源碼位置:src/runtime/hashmap.go
  3. 每次遍歷map順序是否一致?不一致,每次遍歷會隨機個數,經過隨機數來決定從哪一個元素開始。

寫的倉促,不免疏漏,有問題的地方還請批評指正。

參考資料

若是你但願看到源碼的各類細節講解,下面這幾篇是我學習的時候看的,供你參考,但願對你有幫助
https://github.com/qcrao/Go-Questions/tree/master/map
https://github.com/cch123/golang-notes/blob/master/map.md
https://draveness.me/golang-hashmap
https://lukechampine.com/hackmap.html

 

 

做者:LinkinStar

未經容許,不得轉載

相關文章
相關標籤/搜索