go map那些事儿

1.数据结构

hashmap的定义位于 src/runtime/hashmap.go 中golang

// A header for a Go map.
type hmap struct {
	count     int // 元素的个数
	flags     uint8 // 状态标记,标记map当前状态,是否正在写入
	B         uint8   // 能够最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子
	noverflow uint16 // 溢出的个数
	hash0     uint32 // 哈希种子

	buckets    unsafe.Pointer // 桶的地址
	oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容
	nevacuate  uintptr        // 迁移进度,小于nevacuate的已经迁移完成

	extra *mapextra // optional fields
}
复制代码

桶的结构数组

// A bucket for a Go map.
type bmap struct {
        //每一个元素hash值的高8位,若是tophash[0] < minTopHash,表示这个桶的搬迁状态
	tophash [bucketCnt]uint8
        // 接下来是8个key、8个value,可是咱们不能直接看到;为了优化对齐,go采用了key放在一块儿,value放在一块儿的存储方式,
        // 再接下来是hash冲突发生时,下一个溢出桶的地址
}
复制代码

整个hmap结构以下markdown

image.png

2 map的建立、插入、查找、删除、扩容

2.1 建立

map的建立比较简单,在参数校验以后,须要找到合适的B来申请桶的内存空间,接着即是穿件hmap这个结构,以及对它的初始化。数据结构

image.png

2.2 查找

image.png

2.3 插入

image.png

2.4 删除

image.png

2.5 扩容

(1)判断是否须要扩容oop

func (h *hmap) growing() bool {
    return h.oldbuckets != nil
}
复制代码

若是oldbuckets不为空则表示正在扩容。什么时候h.oldbuckets不为nil呢?在分配assign逻辑中,当没有位置给key使用,并且知足测试条件(装载因子>6.5或有太多溢出通)时,会触发hashGrow逻辑:测试

func hashGrow(t *maptype, h *hmap) {
    //判断是否须要sameSizeGrow,不然"真"扩
    bigger := uint8(1)
    if !overLoadFactor(int64(h.count), h.B) {
        bigger = 0
        h.flags |= sameSizeGrow
    }
        // 下面将buckets复制给oldbuckets
    oldbuckets := h.buckets
    newbuckets := newarray(t.bucket, 1<<(h.B+bigger))
    flags := h.flags &^ (iterator | oldIterator)
    if h.flags&iterator != 0 {
        flags |= oldIterator
    }
    // 更新hmap的变量
    h.B += bigger
    h.flags = flags
    h.oldbuckets = oldbuckets
    h.buckets = newbuckets
    h.nevacuate = 0
    h.noverflow = 0
        // 设置溢出桶
    if h.overflow != nil {
        if h.overflow[1] != nil {
            throw("overflow is not nil")
        }
// 交换溢出桶
        h.overflow[1] = h.overflow[0]
        h.overflow[0] = nil
    }
}
复制代码

在assign和delete操做中,都会触发扩容growWork:优化

func growWork(t *maptype, h *hmap, bucket uintptr) {
    // 搬迁旧桶,这样assign和delete都直接在新桶集合中进行
    evacuate(t, h, bucket&h.oldbucketmask())
        //再搬迁一次搬迁过程当中的桶
    if h.growing() {
        evacuate(t, h, h.nevacuate)
    }

}
复制代码

(2)搬迁过程ui

通常来讲,新桶数组大小是原来的2倍(在!sameSizeGrow()条件下),新桶数组前半段能够"类比"为旧桶,对于一个key,搬迁后落入哪个索引中呢?spa

假设旧桶数组大小为2^B, 新桶数组大小为2*2^B,对于某个hash值X
若 X & (2^B) == 0,说明 X < 2^B,那么它将落入与旧桶集合相同的索引xi中;
不然,它将落入xi + 2^B中
复制代码

例如,对于旧B = 3时,hash1 = 4,hash2 = 20,其搬迁结果相似这样。.net

image.png

代码逻辑以下

image.png

3 难点

3.1 为何会有等量扩容

扩容有两个条件:1.负载因子超过阈值;2.使用了太多溢出桶。插入删除,由于删除没有移动元素,除了在末尾以外,新增元素会跳过被删的空元素。所以常常在同一个桶上插入删除会形成这个桶的数据过于稀疏,须要等来给你扩容。

3.2 扩容为何不是当即执行

由于map中可能会保存大量数据,一次性迁移完全部数据涉及到申请大量内存和老数据迁移,若是锁表则会影响用户使用,所以扩容只是作了一个标记,并无真正的申请内存和迁移数据。

3.3 扩容涉及到数据迁移,怎么迁移

迁移数据是增量的过程,即下次放问到了哪一个元素就迁移那个元素,迁移是按桶为单位,直到全部的桶都迁移完成才算迁移完。

当访问某个桶的时候会判断是否正在迁移,若是访问老桶,若是是双倍容量扩容,则把桶的大小除以2,访问老桶,这里须要判断老桶是否迁移完成,若是迁移完成了则访问新桶evacuated(oldb)为true则表示迁移完成

func evacuated(b *bmap) bool {
	h := b.tophash[0]
	return h > emptyOne && h < minTopHash
}
复制代码

当插入某个元素,若是正在迁移,则迁移这个桶,而且当前元素插入新桶

一个桶中的元素是从头至尾迁移,会从新计算它的位置。原来在x处的元素可能到了新桶的x位置,也多是在2^B+x的位置

4 参考

【1】Golang map底层实现原理解析
【2】解剖Go语言map底层实现
【3】哈希表
【4】Golang Map实现原理
【5】Golang源码解析

相关文章
相关标签/搜索