map結構
總體爲一個數組,數組每一個元素能夠理解成一個槽,槽是一個鏈表結構,槽的每一個節點可存8個元素,搞清楚了map的結構,想一想對應的增刪改查操做也不是那麼難
1:槽大小計算&hash算法
咱們能夠簡單的理解成:槽大小爲1<<N,每一個元素計算出一個hash值hashCode,hash到這些槽中,hash算法:hashCode&1<<N-1,恰好和槽的範圍徹底重合
關於hash衝突,那是必須的,當你指定預計元素個數時,預計平均一個槽6.5個元素,據我觀察好多語言的hash都是採用hashCode&1<<N-1的hash方式,多是由於實現簡單,元素分佈比較均勻
hashCode的計算請參考文件:src/runtime/hash64.go src/runtime/hash32.go
2:容量計算和擴容
m := make(map[string]int, hint) func overLoadFactor(count int64, B uint8) bool { return count >= 8 && float32(count) >= 6.5*float32((uint64(1)<<B)) } b := uint8(0) //最大128位足夠了 for ; overLoadFactor(hint, b); b++ { } func makeBucketArray(t *maptype, b uint8) (buckets unsafe.Pointer, nextOverflow *bmap) { base := uintptr(1 << b) nbuckets := base if b >= 4 { nbuckets += 1 << (b - 4) //多分配了1/16的空間,當某個槽滿了以後能夠能夠從這裏多取出一個節點 sz := t.bucket.size * nbuckets up := roundupsize(sz) if up != sz { nbuckets = up / t.bucket.size } } buckets = newarray(t.bucket, int(nbuckets)) if base != nbuckets { nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) last.setoverflow(t, (*bmap)(buckets)) } return buckets, nextOverflow }
初始化容量和擴容都是調上面的方法makeBucketArray
舉個例子:m := make(map[int][string], 10),經過上面計算b的方法得出b=2,即4=1<<2個槽,每一個槽一個節點,每一個節點可容納8個元素,總共可容納32個元素,可容納指望的10個元素
擴容基本採起2*N + N/16的方式(2倍擴容),多出來的N/16用於槽滿的狀況,新增的節點就從這多出來的地方取,若是多出來的槽也用完了就直接new新的內存
槽大小(t.bucketsize)應該是提早就計算好了的,每一個槽能容納8個元素,當槽滿以後,若是還有新增到這個槽的元素,會新增一個槽,以鏈表的方式連在這個槽的後面
擴容後數據怎麼複製過來
go並無採用一會兒就將數據所有複製過來的方式,若是數據不少仍是很是耗時的,而是在寫數據的時候若是命中一個老槽,就將這個槽中的全部數據從新hash到新的數據結果中,在擴容過程當中,新老數據結構同時提供數據的查詢,寫數據的時候只會寫入新的數據結構中,同時將命中老數據結構對應槽中的數據從新hash到新的數據結構中。
擴容條件
1:已經處於擴容狀態就不能再擴容了,就新老兩個數據結構,沒有第三了
2:元素個數>6.5*槽數量(槽的每一個節點能容納8個元素,可是分佈不可能這麼均勻的),能夠擴容
3:槽節點寫滿的個數到達必定數量也能夠擴容
也就是說,在沒有擴容的狀況下:元素個數太多 || 槽節點滿的數量多,都會觸發擴容
一個槽就是多個節點組成的鏈表,節點數據結構以下:
// A bucket for a Go map. const bucketCnt = 8 type bmap struct { // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. 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. }
節點數據結構:
1:8個標記位的數組,用於標記每一個元素的狀況,如這個位置是否有元素,8個字節
2:連續的8個key,根據key的類型能計算出8個key的大小
3:連續的8個value,根據value的類型能計算出8個value的大小
4:指向下一個節點的指針,8個字節
但從節點的數據結構中只看到了8個標記位的數組,其餘的都沒有寫出來,可是一個節點的大小是能計算出來的,能夠分配一個能容納以上4個元素的空間,在強制轉換成*bmap就行,而後按照地址+偏移量就能計算出每一個元素的地址
value = map[key] 源碼分析
//go:linkname reflect_mapaccess reflect.mapaccess map[key] func reflect_mapaccess(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { val, ok := mapaccess2(t, h, key) if !ok { val = nil } return val } func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) { alg := t.key.alg //計算hashCode hash := alg.hash(key, uintptr(h.hash0)) m := 1<<h.B - 1 //hash到對應的槽,go的hash算法就是:hash&(1<<h.B - 1) b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize))) //老的數據結構對應的槽若是有數據,就從老的取 if c := h.oldbuckets; c != nil { if !h.sameSizeGrow() { m >>= 1 //若是正在擴容,老的容量爲新的容量的一半 } oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize))) if !evacuated(oldb) { b = oldb } } top := uint8(hash >> (sys.PtrSize*8 - 8)) if top < minTopHash { top += minTopHash } for { for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top { //top標記位表示有值 continue } //經過槽首地址+偏移量計算第i個key的位置 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) k = *((*unsafe.Pointer)(k)) //若是第i個key等於咱們要查找的key,就返回第i個value if alg.equal(key, k) { //經過槽首地址+偏移量計算第i個value的位置 v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) v = *((*unsafe.Pointer)(v)) return v, true } } //b指向下一個節點,若是不爲空就繼續遍歷 b = b.overflow(t) if b == nil { return unsafe.Pointer(&zeroVal[0]), false } } }
map[key] = value 源碼分析
//go:linkname reflect_mapassign reflect.mapassign map[key] = value func reflect_mapassign(t *maptype, h *hmap, key unsafe.Pointer, val unsafe.Pointer) { p := mapassign(t, h, key) //這裏會把key寫進去 typedmemmove(t.elem, p, val) //這裏寫value } // Like mapaccess, but allocates a slot for the key if it is not present in the map. func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { alg := t.key.alg hash := alg.hash(key, uintptr(h.hash0)) h.flags |= hashWriting //沒元素就分配1個元素的空間 if h.buckets == nil { h.buckets = newarray(t.bucket, 1) } again: //hash到第bucket個槽 bucket := hash & (uintptr(1)<<h.B - 1) //若是正在擴容,會將老數據結果中對應槽中的全部數據都從新hash到新的數據結構中 if h.growing() { growWork(t, h, bucket) } 計算第bucket個槽的地址 b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize))) top := uint8(hash >> (sys.PtrSize*8 - 8)) if top < minTopHash { top += minTopHash } var inserti *uint8 //標記位 var insertk unsafe.Pointer//key寫在這裏 var val unsafe.Pointer //value寫在這裏 for { for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top { //第i個元素爲空,若是是新增的,新增就往這裏寫數據啊 if b.tophash[i] == empty && inserti == nil { inserti = &b.tophash[i] insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) } continue } k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) if t.indirectkey { k = *((*unsafe.Pointer)(k)) } //若是key相等就是更新數據 if !alg.equal(key, k) { continue } // already have a mapping for key. Update it. if t.needkeyupdate { typedmemmove(t.key, k, key) } //更新數據,找到對應的位置返回就行 val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) goto done } //b指向下一個節點,若是不爲空就繼續遍歷 ovf := b.overflow(t) if ovf == nil { break } b = ovf } //已經處於擴容狀態就不能再擴容了,就新老兩個數據結構,沒有第三了 //元素個數>6.5*槽數量(槽的每一個節點能容納8個元素,可是分佈不可能這麼均勻的),能夠擴容 //槽節點寫滿的個數較多也能夠擴容 if !h.growing() && (overLoadFactor(int64(h.count), h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { hashGrow(t, h) goto again // Growing the table invalidates everything, so try again } if inserti == nil { //當前節點已經滿了,從新分配一個節點,新節點會連在當前節點後面 newb := h.newoverflow(t, b) inserti = &newb.tophash[0] insertk = add(unsafe.Pointer(newb), dataOffset) val = add(insertk, bucketCnt*uintptr(t.keysize)) } //引用類型分配key所需空間,同時把key在槽中對應的位置指向這個空間 if t.indirectkey { kmem := newobject(t.key) *(*unsafe.Pointer)(insertk) = kmem insertk = kmem } //引用類型分配value所需空間,同時把value在槽中對應的位置指向這個空間 if t.indirectvalue { vmem := newobject(t.elem) *(*unsafe.Pointer)(val) = vmem } //將key寫入剛分配的空間 typedmemmove(t.key, insertk, key) //更新標記位 *inserti = top //元素個數+1 h.count++ done: if h.flags&hashWriting == 0 { throw("concurrent map writes") } h.flags &^= hashWriting if t.indirectvalue { val = *((*unsafe.Pointer)(val)) } //直接將val返回了,value爲一個結構體(非指針)map[key].fieldXXX = 1,這樣作是不行的 //由於val做爲返回值會被拷貝,若是val類型結構體(非指針),map[key]返回的就是一個拷貝,並無更新真正的val,而指針類型卻沒有這個問題,若是結構體較大最好以指針的方式存入map return val }