1. 程式人生 > >golang-map分析

golang-map分析

想了解Go內建型別的記憶體佈局的契機,是一次在除錯“不同型別的小物件頻繁建立對gc效能的影響”時發現map的gc效能不佳,而作為對比的包含slice的struct卻很好。這裡總結Go runtime裡map的實現,可以解釋這個問題。

hash table內部結構

Go的map就是hashmap,原始碼在src/runtime/hashmap.go。對比C++用紅黑樹實現的map,Go的map是unordered map,即無法對key值排序遍歷。跟傳統的hashmap的實現方法一樣,它通過一個buckets陣列實現,所有元素被hash到陣列的bucket中,buckets就是指向了這個記憶體連續分配的陣列。B

欄位說明hash表大小是2的指數,即2^B。每次擴容會增加到上次大小的兩倍,即2^(B+1)。當bucket填滿後,將通過overflow指標來mallocgc一個bucket出來形成連結串列,也就是為雜湊表解決衝突問題。

12345678910111213// A header for a Go map.type hmap struct { count int // len()返回的map的大小 即有多少kv對 flags uint8 B uint8 // 表示hash table總共有2^B個buckets hash0 uint32 // hash seed buckets unsafe.Pointer // 按照low hash值可查詢的連續分配的陣列,初始時為16個Buckets.
oldbuckets unsafe.Pointer nevacuate uintptr overflow *[2]*[]*bmap //溢位鏈 當初始buckets都滿了之後會使用overflow}
123456789// A bucket for a Go map.type bmap struct { tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt values. // NOTE: packing all the keys together and then all the values together makes the
// code a bit more complicated than alternating key/value/key/value/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer.}

上圖是一個bucket的資料結構,tophash是個大小為8(bucketCnt)的陣列,儲存了8個key的hash值的高八位值,在對key/value對增刪查的時候,先比較key的hash值高八位是否相等,然後再比較具體的key值。根據官方註釋在tophash陣列之後跟著8個key/value對,每一對都對應tophash當中的一條記錄。最後bucket中還包含指向連結串列下一個bucket的指標。記憶體佈局如下圖。

之所以把所有k1k2放一起而不是k1v1是因為key和value的資料型別記憶體大小可能差距很大,比如map[int64]int8,考慮到位元組對齊,kv存在一起會浪費很多空間。

map相關操作

map初始化

B的初始大小是0,若指定了map的大小hint且hint大於8,那麼buckets會在make時就通過newarray分配好,否則buckets會在第一次put的時候分配。隨著hashmap中key/value對的增多,buckets需要重新分配,每一次都要重新hash並進行元素拷貝。所以最好在初始化時就給map指定一個合適的大小。

makemap有h和bucket這兩個引數,是留給編譯器的。如果編譯器決定hmap結構體和第一個bucket可以在棧上建立,這兩個入參可能不是nil的。

12345678910111213// makemap implemments a Go map creation make(map[k]v, hint)func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap{ B := uint8(0) for ; hint > bucketCnt && float32(hint) > loadFactor*float32(uintptr(1)<<B); B++ { } // 確定初始B的初始值 這裡hint是指kv對的數目 而每個buckets中可以儲存8個kv對 // 因此上式是要找到滿足不等式 hint > loadFactor*(2^B) 最小的B if B != 0 { buckets = newarray(t.bucket, uintptr(1)<<B) } h = (*hmap)(newobject(t.hmap)) return h}

map存值

儲存的步驟和第一部分的分析一致。首先用key的hash值低8位找到bucket,然後在bucket內部比對tophash和高8位與其對應的key值與入參key是否相等,若找到則更新這個值。若key不存在,則key優先存入在查詢的過程中遇到的空的tophash陣列位置。若當前的bucket已滿則需要另外分配空間給這個key,新分配的bucket將掛在overflow連結串列後。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152func mapassign1(t *maptype, h *hmap, key unsafe.Pointer, val unsafe.Pointer) { hash := alg.hash(key, uintptr(h.hash0)) if h.buckets == nil { h.buckets = newarray(t.bucket, 1) }again: //根據低8位hash值找到對應的buckets bucket := hash & (uintptr(1)<<h.B - 1) b := (bmap)(unsafe.Pointer(uintptr(h.buckets) + bucketuintptr(t.bucketsize))) //計算hash值的高8位 top := uint8(hash >> (sys.PtrSize*8 - 8)) for { //遍歷每一個bucket 對比所有tophash是否與top相等 //若找到空tophash位置則標記為可插入位置 for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top { if b.tophash[i] == empty && inserti == nil { inserti = &b.tophash[i] } continue } //當前tophash對應的key位置可以根據bucket的偏移量找到 k2 := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) if !alg.equal(key, k2) { continue } //找到符合tophash對應的key位置 typedmemmove(t.elem, v2, val) goto done } //若overflow為空則break ovf := b.overflow(t) } // did not find mapping for key. Allocate new cell & add entry. if float32(h.count) >= loadFactor*float32((uintptr(1)<<h.B)) && h.count >= bucketCnt { hashGrow(t, h) goto again // Growing the table invalidates everything, so try again } // all current buckets are full, allocate a new one. if inserti == nil { newb := (*bmap)(newobject(t.bucket)) h.setoverflow(t, b, newb) inserti = &newb.tophash[0] } // store new key/value at insert position kmem := newobject(t.key) vmem := newobject(t.elem) typedmemmove(t.key, insertk, key) typedmemmove(t.elem, insertv, val) *inserti = top h.count++}

hash Grow擴容和遷移

在往map中存值時若所有的bucket已滿,需要在堆中new新的空間時需要計算是否需要擴容。擴容的時機是count > loadFactor(2^B)。這裡的loadfactor選擇為6.5。擴容時機的物理意義的理解 在沒有溢位時hashmap總共可以儲存8(2^B)個KV對,當hashmap已經儲存到6.5(2^B)個KV對時表示hashmap已經趨於溢位,即很有可能在存值時用到overflow連結串列,這樣會增加hitprobe和missprobe。為了使hashmap保持讀取和超找的高效能,在hashmap快滿時需要在新分配的bucket中重新hash元素並拷貝,原始碼中稱之為evacuate。

overflow溢位率是指平均一個bucket有多少個kv的時候會溢位。bytes/entry是指平均存一個kv需要額外儲存多少位元組的資料。hitprobe是指找到一個存在的key平均需要找多少次。missprobe是指找到一個不存在的key平均需要找多少次。選取6.5是為了平衡這組資料。

loadFactor%overflowbytes/entryhitprobemissprobe
4.002.1320.773.004.00
4.504.0517.303.254.50
5.006.8514.773.505.00
5.5010.5512.943.755.50
6.0015.2711.674.006.00
6.5020.9010.794.256.50
7.0027.1410.154.507.00
7.5034.039.734.757.50
8.0041.109.405.008.00

但這個遷移並沒有在擴容之後一次性完成,而是逐步完成的,每一次insert或remove時遷移1到2個pair,即增量擴容。增量擴容的原因 主要是縮短map容器的響應時間。若hashmap很大擴容時很容易導致系統停頓無響應。增量擴容本質上就是將總的擴容時間分攤到了每一次hash操作上。由於這個工作是逐漸完成的,導致資料一部分在old table中一部分在new table中。old的bucket不會刪除,只是加上一個已刪除的標記。只有當所有的bucket都從old table裡遷移後才會將其釋放掉。

相關推薦

golang-map分析

想了解Go內建型別的記憶體佈局的契機,是一次在除錯“不同型別的小物件頻繁建立對gc效能的影響”時發現map的gc效能不佳,而作為對比的包含slice的struct卻很好。這裡總結Go runtime裡map的實現,可以解釋這個問題。hash table內部結構Go的map就是hashmap,原始碼在src/r

golang map

sea enter htm html 集合 elong ade init school Our friend Monk has been made teacher for the day today by his school professors . He is goin

golang---map類型

線性 int range fun pac map類型 語言 獲取 ack map 類似其它語言中的哈希表或字典,以key-value形式存儲數據 key必須是支持==或!=比較運算的類型,不可以是函數、map或slice Map查找比線性搜索快很多,但比使用索引訪問數據的

Golang原始碼分析atomic.Once

package sync import ( "sync/atomic" ) type Once struct { m Mutex done uint32 } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o

golang map 宣告,賦值

// 先宣告mapvar m1 map[string]string// 再使用make函式建立一個非nil的map,nil map不能賦值m1 = make(map[string]string)// 最後給已宣告的map賦值m1["a"] = "aa"m1["b"] = "bb" // 直接建立

golang map的判斷,刪除

golang map的判斷,刪除  map是一種key-value的關係,一般都會使用make來初始化記憶體,有助於減少後續新增操作的記憶體分配次數。假如一開始定義了話,但沒有用make來初始化,會報錯的。 package main import ( "fmt"

Golang map 如何進行刪除操作?

  Cyeam 關注 2017.11.02 10:02* 字數 372 閱讀 2784評論 0喜歡 3 map 的刪除操作 Golang 內建了雜湊表,總體上是使用雜湊連結串列實現的,如果出現雜湊衝突,就

golang逃逸分析和競爭檢測

最近在線上發現一塊程式碼邏輯在執行N次耗時波動很大1ms~800ms,最開始以為是gc的問題,對程式碼進行逃逸分析,看哪些變數被分配到堆上了,後來發現是併發程式設計時對一個切片併發的寫,導致存在競爭,類似下面的程式碼 func main() { //var count int array :

Golang-map

map和其他語言的hashmap是一樣的,是一個kv的資料集合,是按照雜湊演算法得到k的一個整數,將v存到一個數組的k位。 1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 func main() { 8 9 map1 :=

Golang-bootstrap分析

這篇部落格主要分析golang程式的載入程式啟動流程。 1. 環境 要分析runtime相關內部機制,首先從系統啟動開始。首先準備分析環境:golang、OS、gdb 2. 載入程式巨集觀流程 在go程式碼裡面,使用者邏輯從main.main()開始,那麼

Golang map有序化

要使得Map有序化,我們必須要對map的key進行排序,我們可以使用sort.Strings函式對字串進行排序。 package main import ( "fmt" "sort" ) fun

【我的區塊鏈之路】- golang原始碼分析之select的實現

最近本人再找工作,恩,雖然本人使用go有2年左右了,但是其實還只是停留在語言使用的技巧位面,語言的很多底層實現機制還不是很清楚的,所以面試被問到很多底層,就很懵逼。這篇文章主要是自己對go學習的筆記。(本人還是一隻菜雞,各位海涵) 文章參考: 那麼se

【我的區塊鏈之路】- golang原始碼分析之協程排程器底層實現( G、M、P)

本人的原始碼是基於go 1.9.7 版本的哦! 緊接著之前寫的 【我的區塊鏈之路】- golang原始碼分析之select的底層實現 和 【我的區塊鏈之路】- golang原始碼分析之channel的底層實現 我們這一次需要對go的排程器做一番剖析。

【我的區塊鏈之路】- golang原始碼分析之channel的底層實現

【轉載請標明出處】https://blog.csdn.net/qq_25870633/article/details/83388952 接上篇文章 【我的區塊鏈之路】- golang原始碼分析之select的底層實現 我這裡因為面試的時候也有被問到過 channel的底層實現

【我的區塊鏈之路】- golang原始碼分析之select的底層實現

【轉載請標明出處】https://blog.csdn.net/qq_25870633/article/details/83339538 最近本人再找工作,恩,雖然本人使用go有2年左右了,但是其實還只是停留在語言使用的技巧位面,語言的很多底層實現機制還不是很清楚的,所以面試被問到很多底層,就很懵

除錯技巧 —— 如何利用windbg + dump + map分析程式異常

Microsoft (R) Windows Debugger Version 6.11.0001.404 X86 Copyright (c) Microsoft Corporation. All rights reserved. Loading Dump File [C:\Test\Release\Log\

golang map的遍歷

遍歷key package main import ( "fmt" ) func main() { var mymap map[string]string

golang map併發讀寫

對應報錯:fatal error: concurrent map writesfatal error: concurrent map read and map writehttps://wrfly.kfd.me/posts/read-and-write-in-high-con

golang 原始碼分析之URL編碼規範

首先看一下url編碼規範: backspace %08 tab %09 linefeed %0A creturn %0D space

golang map 用原生range遍歷不能保證順序輸出

按照之前我對map的理解,map中的資料應該是有序二叉樹的儲存順序,正常的遍歷也應該是有序的遍歷和輸出,但實際試了一下,卻發現並非如此,網上查了下,發現從Go1開始,遍歷的起始節點就是隨機了,當然隨機到什麼程度還不清楚。 package main import ( "