哈希表介绍
一个映射,也成为关联数组,其实是一个由唯一键组成的集合,而每个键必然关联一个特定的值。这种键到值的关联关系称为映射,若在键到值的关联使用hash计算,就是哈希表,映射至少支持三个操作:
-
Add (Key,Value)
-
Remove(key)
-
value = LookUp (key)
Map的本质
map的底层结构
Golang的map使用溢出桶(链地址法)解决hash冲突
Map结构
type hmap struct {
count int // 代表map中元素的数量
flags uint8 // 代表当前map的状态
B uint8 // 2^B表示当前map中桶的数量
noverflow uint16 // map中溢出桶的数量
hash0 uint32 // 是哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
buckets unsafe.Pointer // 指向map对应的桶数组的指针
oldbuckets unsafe.Pointer // 存储未转移完毕的旧桶,扩容时非空
nevacuate uintptr// 扩容时使用,用于标记当前旧桶中小于 nevacuate 的数据都已经转移到了新桶中
extra *mapextra // 存储map中的溢出桶
}
桶结构
type bmap struct {
// bucketCnt=8
tophash [bucketCnt]uint8
}
溢出桶
type mapextra struct {
overflow *[]*bmap // buckets 的溢出桶
oldoverflow *[]*bmap // oldbukets 的溢出桶
nextOverflow *bmap // 指向下一个溢出桶
}
Map源码
创建
创建map
当使用make
创建map时,只有指定的元素个数大于8时,或者使用字面量创建时元素个数>8时才会调用runtime.makemap
函数进行map的创建。否则,将调用makemap_small
函数创建hmap
结构,但此时不会分配桶,而是在赋值(runtime.mapassign
)操作时分配桶。
m := make(map[int8]int8, 9)
m1 := map[int8]int8{
1: 1, 2: 2,
3: 3, 4: 4,
5: 5, 6: 6,
7: 7, 8: 8,
9: 9,
}
创建流程
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 计算内存分配量时,使用最均匀的情况下的散列分布
// hint:触发makemap函数时,传入的第二个参数,若为9,则9个元素可能会分布到不同的9个桶中
// t.bucket.size:每个桶占用的字节,其大小等于key*8+value*8+tophash(固定8字节)+overflow指针大小(根据机器字大小4或8字节)
// 该步骤主要计算内存溢出的可能性,若可能溢出,将hint置零
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 初始化hmap结构体
if h == nil {
h = new(hmap)
}
// 取得hash种子,hash种子的计算参与存放在当前m中
h.hash0 = fastrand()
// 计算出需要的桶的个数
B := uint8(0)
// 根据hint个元素,循环计算出需要的桶数量
// 因为后续涉及到真正的内存分配,所以这里按照最近凑的散列分布,即hint/8来控制桶的数量
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 开始为桶分配内存
if h.B != 0 {
var nextOverflow *bmap
// makemap函数中的桶分配由makeBucketArray函数来实现
// 主要任务:1.分配桶大小 2.若需要预分配溢出桶,则分配
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil { // 若分配了溢出桶
// 将预分配的溢出桶赋值给hmap
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
桶分配及溢出桶预分配
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
// 需要分配的桶数量
base := bucketShift(b)
nbuckets := base
// 如果桶的数量>b^4可能需要溢出桶
if b >= 4 {
// 在桶的数量上加上预估溢出桶数量
nbuckets += bucketShift(b - 4)
// 计算出桶需要的内存空间
sz := t.bucket.size * nbuckets
// 因为go将内存划分为不同的大小进行管理,所以这里内存管理器会返回up字节的内存大小,up>=sz
up := roundupsize(sz)
if up != sz {
// 计算出分配的内存可以容纳多少个桶
nbuckets = up / t.bucket.size
}
}
// 当hash表所有键值被删除或该hash表被回收时dirtyalloc!=nil
if dirtyalloc == nil {
// 分配bucket数组
buckets = newarray(t.bucket, int(nbuckets))
} else {// 执行hash内存回收操作
buckets = dirtyalloc
size := t.bucket.size * nbuckets
// 判断bucket是否存在指针对象
if t.bucket.ptrdata != 0 {
// 清楚包含指针类型元素占用的空间
memclrHasPointers(buckets, size)
} else {
// 清楚非指针类型元素占用的空间
memclrNoHeapPointers(buckets, size)
}
}
// 判断是否需要分配溢出桶
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
}
赋值/更新
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
// 因为map是并发不安全的,因此在并发读写的情况下会出现警告
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 通过key和hash种子生成hash值
hash := t.hasher(key, uintptr(h.hash0))
// 将当前map标记为正在写
h.flags ^= hashWriting
// 当map中的所有键值对都被删除,内存可能被回收,因此这里重新分配一个bucket
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 通过hash和桶个数进行与运算,得出桶的位置
bucket := hash & bucketMask(h.B)
// 判断当前map是否处于扩容状态,执行一个桶的搬迁
if h.growing() {
// 执行一次扩容操作
growWork(t, h, bucket)
}
// 根据map中桶的首地址计算出目标桶的地址
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
// 返回hash的高8位
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:
for {
for i := uintptr(0); i < bucketCnt; i++ {
// 如果第i个tophash 与 key计算出的tophash不同
if b.tophash[i] != top {
// 判断tophash[i]是否为空,且inserti是否为空,当此条件为true时,标识,当前tophash为第一个遇到的空tophash
if isEmpty(b.tophash[i]) && inserti == nil {
// 记录当前tophash的内存地址
inserti = &b.tophash[i]
// 记录当前tophash对应的key的内存地址
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
// 记录当前tophash对应的value的内存地址
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
// 若当前tophash 为emptyRest,表示后面的tophash都为空,而第一个空tophash已经被记录,此时则跳出循环
if b.tophash[i] == emptyRest {
break bucketloop
}
// 继续循环
continue
}
// 运行至此处表示找到和当前key相同的tophash
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 如果key不相同 遍历下一个tophash
if !t.key.equal(key, k) {
continue
}
// already have a mapping for key. Update it.
// 走到此处就可以确定当前为更新操作了
// tophash 相同 且key也相同时更新该key的值
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
// 的value内存所在的地址
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
// 当前桶遍历完,尝试获取溢出桶,若溢出桶不为空,则遍历溢出桶
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 判断是否需要扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again
}
// 没有桶和溢出桶都没有找到合适的位置,创建一个新的溢出桶来存放
if inserti == nil {
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
// 若key为指针类型,分配新内存,将insertK指向新分配的内存
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
// 若value为指针类型,分配新内存,将elem指向新分配的内存
if t.indirectelem() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(elem) = vmem
}
typedmemmove(t.key, insertk, key)
*inserti = top
h.count++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// 清除标记
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
取值
map取值的函数有两个,runtime.mapaccess1
、runtime.mapaccess2
区别在于后者返回两个参数v,bool,用以判断map中是否存在相应的key。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if msanenabled && h != nil {
msanread(key, t.key.size)
}
// map为空或者元素个数为0 返回元素类型的零值
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 根据key 和hash种子计算出key的对应hash
hash := t.hasher(key, uintptr(h.hash0))
// 当前hash拥有的桶的个数
m := bucketMask(h.B)
// 获取 hash&m 计算出桶的位置
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 判断是否处于扩容,即 h.oldbuckets 不为nil
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
// 取出旧桶
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
// 若该旧桶尚未迁移
if !evacuated(oldb) {
b = oldb
}
}
// 计算出tophash 也就是hash的高八位
top := tophash(hash)
// 开始通过tophash遍历 桶+溢出桶
bucketloop:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
// 依次判断tophash
if b.tophash[i] != top {
// 表示后续tophash都为空,退出循环,取值失败
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// 取出map中tophash对应的key值
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 对比取值的k与tophash对应的key是否相同
if t.key.equal(key, k) {
// 找到key对应value值
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
// 若间接引用,需要进行解引用
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
删除
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
// 确保没有并发操作map
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 计算hash值
hash := t.hasher(key, uintptr(h.hash0))
// 标记当前map处于写状态
h.flags ^= hashWriting
// 计算出该hash可能存放在哪个桶中
bucket := hash & bucketMask(h.B)
// 若map处于扩容状态
if h.growing() {
// 执行一次扩容任务
growWork(t, h, bucket)
}
// 找到桶的地址
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
bOrig := b
// 计算出tophash
top := tophash(hash)
search:
// 遍历桶+溢出桶
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
// 依次对比桶中的tophash
if b.tophash[i] != top {
// 若桶中tophash为emptyRest,桶中后续空间不存在key
if b.tophash[i] == emptyRest {
// 没找到,退出
break search
}
continue
}
// 计算key的地址
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
// 若当前key与想要删除的key不同,继续遍历
if !t.key.equal(key, k2) {
continue
}
// 清除key
if t.indirectkey() {// 间接引用
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
// 计算value地址
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
// 清除vlaue
if t.indirectelem() {// 间接引用
*(*unsafe.Pointer)(e) = nil
} else if t.elem.ptrdata != 0 {// 指针
memclrHasPointers(e, t.elem.size)
} else {// 非指针
memclrNoHeapPointers(e, t.elem.size)
}
// 将该tophash设为emptyOne
b.tophash[i] = emptyOne
// 当前为最后一个桶或者桶中tophash都为emptyRest,跳出循环
if i == bucketCnt-1 {
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
// 向上查找,将tophash为emptyOne的tophash标记为emptyRest
for {
b.tophash[i] = emptyRest
if i == 0 {
if b == bOrig {
break // beginning of initial bucket, we're done.
}
// Find previous bucket, continue at its last entry.
c := b
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
i = bucketCnt - 1
} else {
i--
}
if b.tophash[i] != emptyOne {
break
}
}
notLast:
h.count--
// 若map中元素被清空
if h.count == 0 {
// 重置hash算法
h.hash0 = fastrand()
}
break search
}
}
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
}
Map 扩容
扩容模式判断
map扩容由赋值操作runtime.mapassign
触发。
map扩容分两种:
-
翻倍扩容:大多数桶都处于满负载状态
元素数量达到map负载因子6.5:count/buckets>6.5
func overLoadFactor(count int, B uint8) bool { return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen) }
-
等量扩容:溢出桶过多,元素过少
h.B<=15,溢出桶>=2^B
h.B>15,溢出桶>=2^15
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool { if B > 15 { B = 15 } return noverflow >= uint16(1)<<(B&15) }
扩容前的准备
并没有直接进行扩容,而是遵循写时复制原则,只有在对数据操作时才进行扩容。
func hashGrow(t *maptype, h *hmap) {
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
}
// 设置B
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 {
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
}
}
执行扩容
具体的扩容操作被分散在对map的操作中(赋值、取值、删除),这样做的好处是避免了,一次性完成桶搬迁所产生的性能抖动问题。
执行扩容任务
func growWork(t *maptype, h *hmap, bucket uintptr) {
// 搬迁命中的桶
evacuate(t, h, bucket&h.oldbucketmask())
// 若搬迁任务没有完成,在游标处再搬迁一个桶
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}
扩容具体实现
新桶数量-1&旧桶位置=旧桶在新桶中的位置
并发安全
通过源码可以观察到Go并没有为map进行并发保护,若并发操作map会触发painc。
sync.map
提供并发安全的map。
func main() {
m := make(map[int]int)
go func() {
for i := 0; i < 1000; i++ {
m[i] = i + 1
}
}()
go func() {
for i := 0; i < 1000; i++ {
fmt.Println(m[i])
}
}()
time.Sleep(3 * time.Second)
}