【GO】map 实现原理

本文基于小徐先生的编程世界学习map

1.基本用法

1.核心特征

(1)存储基于 key-value 对映射的模式;
(2)基于 key 维度实现存储数据的去重;
(3)读、写、删操作控制,时间复杂度 O(1).

2.初始化方法

myMap1 := make(map[int]int,2)
myMap2 := make(map[int]int)
myMap3 :=map[int]int{
  1:2,
  3:4,
}

3.key 的类型要求

map 中,key 的数据类型必须为可比较的类型,slice、map、func不可比较

4.读

读 map 分为下面两种方式:

v1 := myMap[10]

第一种方式是直接读,倘若 key 存在,则获取到对应的 val,倘若 key 不存在或者 map 未初始化,会返回 val 类型的零值作为兜底.

v2,ok := myMap[10]

第二种方式是读的同时添加一个 bool 类型的 flag 标识是否读取成功. 倘若 ok == false,说明读取失败, key 不存在,或者 map 未初始化.

此处同一种语法能够实现不同返回值类型的适配,是由于代码在汇编时,会根据返回参数类型的区别,映射到不同的实现方法.

5.写

myMap[5] = 6

写操作的语法如上. 须注意的一点是,倘若 map 未初始化,直接执行写操作会导致 panic

6.删除

delete(myMap,5)

执行 delete 方法时,倘若 key 存在,则会从 map 中将对应的 key-value 对删除;倘若 key 不存在或 map 未初始化,则方法直接结束,不会产生显式提示.

7. 遍历

遍历分为下面两种方式:

for k,v := range myMap{
  // ...
}

基于 k,v 依次承接 map 中的 key-value 对;

for k := range myMap{
  // ...
}

基于 k 依次承接 map 中的 key,不关注 val 的取值.
需要注意的是,在执行 map 遍历操作时,获取的 key-value 对并没有一个固定的顺序,因此前后两次遍历顺序可能存在差异.

8.并发冲突

map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.
具体规则是:
(1)并发读没有问题;
(2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;
(3)读的时候发现其他 goroutine 在并发写,抛出 fatal error;
(4)写的时候发现其他 goroutine 在并发写,抛出 fatal error.

并发安全的 map 通常可以使用以下几种方法:

  1. sync.Map:Go 语言标准库提供的并发安全的 map 实现,适用于读多写少的场景。
  2. 使用互斥锁(sync.Mutex):手动在对 map 进行读写操作时加锁。
  3. 使用读写锁(sync.RWMutex):在读多写少的场景下,使用读写锁可以提高并发性能。

2.核心原理

map 又称为 hash map,在算法上基于== hash表== 实现 key 的映射和寻址;在数据结构上基于桶数组实现 key-value 对的存储.
hash是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间,因此不同输入可能会映射成相同的输出结果.
hash的特点:
(1)hash 的可重入性:相同的 key,必然产生相同的 hash 值;
(2)hash 的离散性:只要两个 key 不相同,不论其相似度的高低,产生的 hash 值会在整个输出域内均匀地离散化;
(3)hash 的单向性:企图通过 hash 值反向映射回 key 是无迹可寻的.
(4)hash 冲突:由于输入域(key)无穷大,输出域(hash 值)有限,因此必然存在不同 key 映射到相同 hash 值的情况,称之为 hash 冲突.

3.数据结构

hmap

type hmap struct {
    count     int     //当前保存元素个数
    flags     uint8
    B         uint8     //bucket数组大小->2^B
    noverflow uint16 //map 中溢出桶的数量;
    hash0     uint32 //hash 随机因子,生成 key 的 hash 值时会使用到
    buckets    unsafe.Pointer //bucket数组,长度为2^B
    oldbuckets unsafe.Pointer //老旧的bucket数组,在扩容时出现
    nevacuate  uintptr     //  扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中;
    extra *mapextra //预申请的溢出桶.
}

bmap

bucket数据结构:
const bucketCnt = 8
type bmap struct {
    tophash [bucketCnt]uint8
    data []byte//k0/..../k7/val1/.../val7每个bucket可以存储8个kv对
    overflow *bmap
}
  • tophash:长度为8的整形数组,存储hash值的高位
  • data:存放kv数据
  • overflow:指向下一个bucket将所有冲突的键连接

在代码层面只展示了 tophash 部分,但由于 tophash、key 和 val 的数据长度固定,因此可以通过内存地址偏移的方式寻找到后续的 key 数组、val 数组以及溢出桶指针

4.构造方法

1.makemap

func makemap(t *maptype, hint int, h *hmap) *hmap {
    //(1)hint 为 map 拟分配的容量;在分配前,会提前对拟分配的内存大小进行判断,
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    //倘若超限,会将 hint 置为零;
    if overflow || mem > maxAlloc {
        hint = 0
    }

//(2)通过 new 方法初始化 hmap;
    if h == nil {
        h = new(hmap)
    }
  //(3)调用 fastrand,构造 hash 因子:hmap.hash0;
    h.hash0 = fastrand()


//致上基于 log2(B) >= hint 的思路,计算桶数组的容量 B;
    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B

//(5)调用 makeBucketArray 方法,初始化桶数组 hmap.buckets;
    if h.B != 0 {
        var nextOverflow *bmap
        h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)6)倘若 map 容量较大,会提前申请一批溢出桶 hmap.extra.
        if nextOverflow != nil {
            h.extra = new(mapextra)
            h.extra.nextOverflow = nextOverflow
        }
    }


    return 
    }

2.overLoadFactor

(1)倘若 map 预分配容量小于等于 8,B 取 0,桶的个数为 1;
(2)保证 map 预分配容量小于等于桶数组长度 * 6.5.

const loadFactorNum = 13
const loadFactorDen = 2
const goarch.PtrSize = 8
const bucketCnt = 8


func overLoadFactor(count int, B uint8) bool {
    return count > bucketCnt && uintptr(count) > 13*(bucketShift(B)/2)
}


func bucketShift(b uint8) uintptr {
    return uintptr(1) << (b & (goarch.PtrSize*8 - 1))

5.读流程

map 读流程主要分为以下几步:
(1)根据 key 取 hash 值;
(2)根据 hash 值对桶数组取模,hash值低位确定所在的桶;
(3)沿着桶链表依次遍历各个桶内的 key-value 对;(根据hash的高位获得key的offset)
(4)命中相同的 key,则返回 value;倘若 key 不存在,则返回零值.
map 读操作最终会走进 runtime/map.go 的 mapaccess 方法中,下面开始阅读源码:

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {1)倘若 map 未初始化,或此时存在 key-value 对数量为 0,直接返回零值;
    if h == nil || h.count == 0 {
        return unsafe.Pointer(&zeroVal[0])
    }2)倘若发现存在其他 goroutine 在写 map,直接抛出并发读写的 fatal error;其中,并发写标记,位于 hmap.flags 的第 3 个 bit 位;
    
    if h.flags&hashWriting != 0 {
        fatal("concurrent map read and map write")
    }3)通过 maptype.hasher() 方法计算得到 key 的 hash 值,并对桶数组长度取模,取得对应的桶. 关于 hash 方法的内部实现,golang 并未暴露.
    
    hash := t.hasher(key, uintptr(h.hash0))
    m := bucketMask(h.B)//其中,bucketMast 方法会根据 B 求得桶数组长度 - 1 的值,用于后续的 & 运算,实现取模的效果:
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))4)在取桶时,会关注当前 map 是否处于扩容的流程,
        倘若是的话,需要在老的桶数组 oldBuckets 中取桶,
    if c := h.oldbuckets; c != nil {
        if !h.sameSizeGrow() {
        在取老桶前,会先判断 map 的扩容流程是否是增量扩容,
        倘若是的话,说明老桶数组的长度是新桶数组的一半,需要将桶长度值 m 除以 2.
            m >>= 1
        }
        oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
        通过 evacuated 方法判断桶数据是已迁到新桶还是仍存留在老桶,倘若仍在老桶,需要取老桶进行遍历.
        if !evacuated(oldb) {
            b = oldb
        }
    }
    top := tophash(hash)
bucketloop:
开启两层 for 循环进行遍历流程,
外层基于桶链表,依次遍历首个桶和后续的每个溢出桶,内层依次遍历一个桶内的 key-value 对.
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                if b.tophash[i] == emptyRest {
                内层遍历时,首先查询高 8 位的 tophash 值,看是否和 key 的 top 值匹配.
                倘若不匹配且当前位置 tophash 值为 0,说明桶的后续位置都未放入过元素,当前 key 在 map 中不存在,可以直接打破循环,返回零值.
                    break bucketloop
                }
                continue
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            if t.key.equal(key, k) {
                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 (h *hmap) sameSizeGrow() bool {
    return h.flags&sameSizeGrow != 0
}


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

6.写流程

map 写流程主要分为以下几步:
(1)根据 key 取 hash 值;
(2)根据 hash 值低位确定所在的桶;
(3)倘若 map 处于扩容,则迁移命中的桶,帮助推进渐进式扩容;
(4)沿着桶链表依次遍历各个桶内的 key-value 对;
(5)倘若命中相同的 key,则对 value 中进行更新;
(6)倘若 key 不存在,则插入 key-value 对;
(7)倘若发现 map 达成扩容条件,则会开启扩容模式,并重新返回第(2)步.

map 写操作最终会走进 runtime/map.go 的 mapassign 方法中,下面开始阅读源码:
(1)写操作时,倘若 map 未初始化,直接 panic;
(2)倘若其他 goroutine 在进行写或删操作,抛出并发写 fatal error;
(3)通过 maptype.hasher() 方法求得 key 对应的 hash 值;
(4)通过异或位运算,将 map.flags 的第 3 个 bit 位置为 1,添加写标记;
(5)倘若 map 的桶数组 buckets 为空,则对其进行初始化;
(6)找到当前 key 对应的桶索引 bucket;
(7)倘若发现当前 map 正处于扩容过程,则帮助其渐进扩容,具体内容在第 9 节中再作展开;
(8)从 map 的桶数组 buckets 出发,结合桶索引和桶容量大小,进行地址偏移,获得对应桶 b;
(9)取得 key 的高 8 位 tophash:
(10)提前声明好的三个指针,用于指向存放 key-value 的空槽:
(11)开启两层 for 循环,外层沿着桶链表依次遍历,内层依次遍历桶内的 key-value 对:
(12)倘若 key 的 tophash 和当前位置 tophash 不同,则会尝试将 inserti、insertk elem 调整指向首个空位,用于后续的插入操作.
倘若发现当前位置 tophash 标识为 emtpyRest(0),则说明当前桶链表后续位置都未空,无需继续遍历,直接 break 遍历流程即可.(倘若桶中某个位置的 tophash 标识为 emptyOne(1),说明当前位置未放入元素,倘若为 emptyRest(0),说明包括当前位置在内,此后的位置都为空)
(13)倘若找到了相等的 key,则执行更新操作,并且直接跳转到方法的 done 标志位处,进行收尾处理;
(14)倘若没找到相等的 key,会在执行插入操作前,判断 map 是否需要开启扩容模式. 这部分内容在第 9 节中作展开.
倘若需要扩容,会在开启扩容模式后,跳转回 again 标志位,重新开始桶的定位以及遍历流程.
(15)倘若遍历完桶链表,都没有为当前待插入的 key-value 对找到空位,则会创建一个新的溢出桶,挂载在桶链表的尾部,并将 inserti、insertk、elem 指向溢出桶的首个空位:
(16)将 tophash、key、value 插入到取得空位中,并且将 map 的 key-value 对计数器 count 值加 1;
(17)收尾环节,再次校验是否有其他协程并发写,倘若有,则抛 fatal error. 将 hmap.flags 中的写标记抹去,然后退出方法.

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    if h == nil {
        panic(plainError("assignment to entry in nil map"))
    }
    if h.flags&hashWriting != 0 {
        fatal("concurrent map writes")
    }
    hash := t.hasher(key, uintptr(h.hash0))


    h.flags ^= hashWriting


    if h.buckets == nil {
        h.buckets = newobject(t.bucket) 
    }


again:
    bucket := hash & bucketMask(h.B)
    if h.growing() {
        growWork(t, h, bucket)
    }
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    top := tophash(hash)


    var inserti *uint8
    var insertk unsafe.Pointer
    var elem unsafe.Pointer
bucketloop:
    for {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                if isEmpty(b.tophash[i]) && inserti == nil {
                    inserti = &b.tophash[i]
                    insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                    elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                }
                if b.tophash[i] == emptyRest {
                    break bucketloop
                }
                continue
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            if !t.key.equal(key, k) {
                continue
            }
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            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))
    }


    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    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 {
        fatal("concurrent map writes")
    }
    h.flags &^= hashWriting
    if t.indirectelem() {
        elem = *((*unsafe.Pointer)(elem))
    }
    return
    }

创建溢出桶时:
I 倘若 hmap.extra 中还有剩余可用的溢出桶,则直接获取 hmap.extra.nextOverflow,并将 nextOverflow 调整指向下一个空闲可用的溢出桶;
II 倘若 hmap 已经没有空闲溢出桶了,则创建一个新的溢出桶.
III hmap 的溢出桶数量 hmap.noverflow 累加 1;
IV 将新获得的溢出桶添加到原桶链表的尾部;
V 返回溢出桶.

7.删除数据

map 删楚 kv 对流程主要分为以下几步:
(1)根据 key 取 hash 值;
(2)根据 hash 值对桶数组取模,确定所在的桶;
(3)倘若 map 处于扩容,则迁移命中的桶,帮助推进渐进式扩容;
(4)沿着桶链表依次遍历各个桶内的 key-value 对;
(5)倘若命中相同的 key,删除对应的 key-value 对;并将当前位置的 tophash 置为 emptyOne,表示为空;
(6)倘若当前位置为末位,或者下一个位置的 tophash 为 emptyRest,则沿当前位置向前遍历,将毗邻的 emptyOne 统一更新为 emptyRest.

map 删操作最终会走进 runtime/map.go 的 mapdelete 方法中,下面开始阅读源码:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
    if h == nil || h.count == 0 {
        return
    }
    if h.flags&hashWriting != 0 {
        fatal("concurrent map writes")
    }


    hash := t.hasher(key, uintptr(h.hash0))


    h.flags ^= hashWriting


    bucket := hash & bucketMask(h.B)
    if h.growing() {
        growWork(t, h, bucket)
    }
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    bOrig := b
    top := tophash(hash)
search:
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                if b.tophash[i] == emptyRest {
                    break search
                }
                continue
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            k2 := k
            if t.indirectkey() {
                k2 = *((*unsafe.Pointer)(k2))
            }
            if !t.key.equal(key, k2) {
                continue
            }
            // Only clear key if there are pointers in it.
            if t.indirectkey() {
                *(*unsafe.Pointer)(k) = nil
            } else if t.key.ptrdata != 0 {
                memclrHasPointers(k, t.key.size)
            }
            e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            if t.indirectelem() {
                *(*unsafe.Pointer)(e) = nil
            } else if t.elem.ptrdata != 0 {
                memclrHasPointers(e, t.elem.size)
            } else {
                memclrNoHeapPointers(e, t.elem.size)
            }
            b.tophash[i] = emptyOne
            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
                }
            }
            for {
                b.tophash[i] = emptyRest
                if i == 0 {
                    if b == bOrig {
                        break
                    }
                    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--
            if h.count == 0 {
                h.hash0 = fastrand()
            }
            break search
        }
    }


    if h.flags&hashWriting == 0 {
        fatal("concurrent map writes")
    }
    h.flags &^= hashWritin

8.遍历流程

map 遍历流程开始时,首先会走进 runtime/map.go 的 mapiterinit() 方法当中,此时会对创建 map 迭代器 hiter,并且通过取随机数的方式,决定遍历的起始桶号,以及起始 key-value 对索引号.

type hiter struct {
    key         unsafe.Pointer 
    elem        unsafe.Pointer 
    t           *maptype
    h           *hmap
    buckets     unsafe.Pointer 
    bptr        *bmap         
    overflow    *[]*bmap      
    oldoverflow *[]*bmap      
    startBucket uintptr       
    offset      uint8         
    wrapped     bool         
    B           uint8
    i           uint8
    bucket      uintptr
    checkBucket uintptr
}

9.扩容流程(rehash)

map 的扩容类型分为两类,一类叫做增量扩容,一类叫做等量扩容.
(1)增量扩容
表现:扩容后,桶数组的长度增长为原长度的 2 倍;
目的:降低每个桶中 key-value 对的数量,优化 map 操作的时间复杂度.
条件:负载因子达到6.5
(2)等量扩容
场景:大量的增删后数据集中在一小部分的bucket中,访问效率差
表现:扩容后,桶数组的长度和之前保持一致;但是溢出桶的数量会下降.
目的:提高桶主体结构的数据填充率,减少溢出桶数量,避免发生内存泄漏.
条件:溢出桶的个数达到2^B
(3)渐进扩容
map 采用的是渐进扩容的方式,避免因为一次性的全量数据迁移引发性能抖动.
当每次触发写、删操作时,会为处于扩容流程中的 map 完成两组桶的数据迁移:
(1)一组桶是当前写、删操作所命中的桶;
(2)另一组桶是,当前未迁移的桶中,索引最小的那个桶.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值