前言
map是golang中常用的一个基本数据结构,继上篇的channel源码学习之后,本文学习map的源码相关。
基础知识点
此部分参考自 https://www.zhihu.com/tardis/zm/art/423008350?source_id=1003
map的用法
通过make初始化分配容量,通过m[key] = value
进行设置。
map初始化时为什么要关注容量
容量不够时候就需要涉及到扩容,所以一次分配够了,减少扩容次数,提高性能。
demo:
提升了两倍多的性能。
遍历时的无序性
具体自己先前文章中写过
对于map的操作
具体来说,就是边遍历边新增/删除。删除了的不可能再遍历到,新增的可能再遍历到(不一定)。
原因:map内部实现是一个链式hash表,为了保证无顺序,初始化时会随机一个遍历开始的位置,所以新增的元素被遍历到就变的不确定了,同样删除也是一个道理,但是删除元素后边就不会出现,所以一定不会被遍历到。
另一个点是,他随机并不是完全等概率的随机,第一个元素被选中的概率更高一些。
nil-map的写入操作会引发panic
扩容是在已申请的容量不够用时候,会触发。
但是直接0个容量,没有初始化。那就直接panic了。
map并发安全吗?不安全!
可以使用REMutex或者sync.Map去实现并发安全的map。
底层结构-源码
源码位于src\runtime\map.go中
源码
map底层是一个hmap,其中内容:
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
含义:
count
:map的当前长度,用len(m)输出的就是。
(可以理解为喝slice那边一样,一个len 一个cap)
B
:和桶的多少相关,2^B = 桶的个数。
noverflow
:表示近似溢出桶的数量。
hash0
:哈希因子。
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
之后这两个对应的是hash桶,其中一个是扩容时候所用的。
提高效率的几处精妙设计
key hash值的后B位作为桶index查找桶
key hash值的前8位作为桶内结构体的三个数组(tophash,key,value)的index
桶结构体的tophash复用,既作为tophash使用,也作为标志位使用
灵活的扩容机制
工作流程-源码
创建
通过makemap
函数进行创建。
// 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 {
mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
if overflow || mem > maxAlloc {
hint = 0
}
// initialize Hmap
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// Find the size parameter B which will hold the requested # of elements.
// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign)
// If hint is large zeroing this memory could take a while.
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
扩容
扩容主要分为两种:
1、装载因子已经超过 6.5——放不下了;
2、哈希使用了太多溢出桶——链表太长 太乱了;
放不下了扩容
溢出桶过多整理
即sameSizeGrow
扩容,其本质并不是扩容,而是重新整理,减少链表。
放不下了扩容
即biggerSizeGrow
扩容。当大于6.5时候,B+1,即翻倍桶的个数,之后进行迁移。
func hashGrow(t *maptype, h *hmap) {
// If we've hit the load factor, get bigger.
// Otherwise, there are too many overflow buckets,
// so keep the same number of buckets and "grow" laterally.
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// commit the grow (atomic wrt gc)
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
if h.extra != nil && h.extra.overflow != nil {
// Promote current overflow buckets to the old generation.
if h.extra.oldoverflow != nil {
throw("oldoverflow is not nil")
}
h.extra.oldoverflow = h.extra.overflow
h.extra.overflow = nil
}
if nextOverflow != nil {
if h.extra == nil {
h.extra = new(mapextra)
}
h.extra.nextOverflow = nextOverflow
}
// the actual copying of the hash table data is done incrementally
// by growWork() and evacuate().
}