• Golang---基本类型(map)


      摘要:今天我们来学习 Golang 中的 另外一种常用的数据类型,通过数据结构和源码来分析 golang 中的 map 是如何实现的。

    数据结构

    bucketCntBits = 3
    bucketCnt = 1 << bucketCntBits
    
    // Maximum average load of a bucket that triggers growth is 6.5.
    // Represent as loadFactorNum/loadFactorDen, to allow integer math.
    loadFactorNum = 13
    loadFactorDen = 2
    
    type hmap struct {
        count      int      //哈希表中元素数量
        flags      uint8  //标志位,每一位有特定的含义,比如写标识
        B          uint8  //bucket 的数量,len(buckets) = pow(2, B)
        noverflow  uint16
        hash0      uint32  //传入 Hash 函数的哈希种子
        buckets    *bmap   //指向 bmap 结构体数组的指针
        oldbuckets *bmap   //哈希扩容时候用到,和 Redis 哈希扩容策略相同
        nevacuate  uintptr
        extra      unsafe.Pointer // *mapextra
     }
    
    
    type bmap struct {
        topbits [8]uint8 //存储哈希值的高 8 位
        keys[8] keytype  //存储 8 个 key
        values[8] valuetype  //存储 8 个 value
        pad      uintptr  //内存对齐
        overflow uintptr  //溢出桶指针,能减少扩容频率
    }
    
    // mapextra holds fields that are not present on all maps.
    type mapextra struct {
        // If both key and elem do not contain pointers and are inline, then we mark bucket
        // type as containing no pointers. This avoids scanning such maps.
        // However, bmap.overflow is a pointer. In order to keep overflow buckets
        // alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
        // overflow and oldoverflow are only used if key and elem do not contain pointers.
        // overflow contains overflow buckets for hmap.buckets.
        // oldoverflow contains overflow buckets for hmap.oldbuckets.
        // The indirection allows to store a pointer to the slice in hiter.
        overflow* [] * bmap
        oldoverflow* [] * bmap
    
        // nextOverflow holds a pointer to a free overflow bucket.
        nextOverflow* bmap
    }
    map-struct

    map 的创建

    // 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)
        }
        //生成一个 hash种子
        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 = 0
        B := uint8(0)
        //如果创建 map 时候的时候指定了元素的个数,则会选择一个合适的 B 值
        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
    }
    map-create

    map 的查找

      我们有两种访问 hash 的方式:

    //method1
    val := hash[key]
    //method2
    val, ok := hash[key]

    上面两种方式调用的底层函数是不同的。当接收参数仅为一个时,会调用 runtime.mapaccess1; 当接收函数为两个参数时,会使用 runtime.mapaccess2;这两个的代码实现基本类似,我们就拿 mapaccess1 来进行分析:

    func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
        if raceenabled && h != nil {
            callerpc := getcallerpc()
            pc := funcPC(mapaccess1)
            racereadpc(unsafe.Pointer(h), callerpc, pc)
            raceReadObjectPC(t.key, key, callerpc, pc)
        }
        if msanenabled && h != nil {
            msanread(key, t.key.size)
        }
        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")
        }
        hash := t.hasher(key, uintptr(h.hash0))
        // 如果 B==5, m = 0001 1111, 后面用 m 来定位桶
        // 因为 len(buckets) = pow(2, B), 
        m := bucketMask(h.B)
        //hash&m 得到 key 的 hash值的最后 B位
        // b 通过基地址+偏移量的方式 定位到 桶起始地址
        b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
        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
            }
        }
        //top 为 hash值的 高8位,后续用来定位一个桶中的 bmap 结构(一个桶后面是一个 bmap结构的 链表)
        top := tophash(hash)
    bucketloop:
        //外层循环遍历一个桶中的所有 bmap 结构
        for ; b != nil; b = b.overflow(t) {
            //内层循环遍历每个 bmap结构中的 8 个 key/value
            for i := uintptr(0); i < bucketCnt; i++ {
                //每个 bmap 都有一个 [bucketCnt]uint8 的数组,分别存放每个 key hash值的高8位
                if b.tophash[i] != top {
                    if b.tophash[i] == emptyRest {
                        break bucketloop
                    }
                    continue
                }
                //定位到一个 bmap 中的 key 的位置
                k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                //如果 key 是指针,取出所指向的值
                if t.indirectkey() {
                    k = *((*unsafe.Pointer)(k))
                }
                //如果取出的 k 和要查找的 key 相同(只是根据高8位查找的,存在不相同的情况)
                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])
    }
    map-find

    map 的赋值

    map 的赋值过程基本和上述的查找过程一致,首先用上述思想查找到当前的 key, 如果找到,则更新当前key 对应的 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 {
        if h == nil {
            panic(plainError("assignment to entry in nil map"))
        }
        if raceenabled {
            callerpc := getcallerpc()
            pc := funcPC(mapassign)
            racewritepc(unsafe.Pointer(h), callerpc, pc)
            raceReadObjectPC(t.key, key, callerpc, pc)
        }
        if msanenabled {
            msanread(key, t.key.size)
        }
        if h.flags&hashWriting != 0 {
            throw("concurrent map writes")
        }
        hash := t.hasher(key, uintptr(h.hash0))
    
        // Set hashWriting after calling t.hasher, since t.hasher may panic,
        // in which case we have not actually done a write.
        h.flags ^= hashWriting
    
        //如果当前 map 为空,则创建一个桶
        if h.buckets == nil {
            h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
        }
    
    again:
        //根据key 的 hash值的后B位 定位到所在桶
        bucket := hash & bucketMask(h.B)
        //如果正在扩容,则执行搬迁操作,一次搬一个或者两个(map 的迁移是渐进式的,不是一次迁移全部的 key)
        //这种思想和 Redis 中的 map 迁移思想相同
        if h.growing() {
            growWork(t, h, bucket)
        }
        //定位到当前桶
        b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
        //获取到hash值的高8位
        top := tophash(hash)
    
        var inserti *uint8      //将要插入的位置
        var insertk unsafe.Pointer  //将要插入的 key
        var elem unsafe.Pointer  //将要插入的 value
    bucketloop:
        //下面这两个 for 循环思想和上面查找的思想一致
        //外层对 bmap 链表进行遍历
        //内层对 bmap 中的 8 个 key 进行对比查找
        for {
            for i := uintptr(0); i < bucketCnt; i++ {
                if b.tophash[i] != top {
                    //如果当前槽位没有放置 key, 则准备将该元素放置到该位置,首先进行预赋值,但并未真正插入
                    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
                }
                //如果有匹配的,则进一步比较 key 是否相同
                k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                if t.indirectkey() {
                    k = *((*unsafe.Pointer)(k))
                }
                //如果 key 不相同,则接着往下找
                if !t.key.equal(key, k) {
                    continue
                }
                // already have a mapping for key. Update it.
                //如果 key 相等,则更新该 key 对应的 value
                if t.needkeyupdate() {
                    typedmemmove(t.key, k, key)
                }
                elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                goto done
            }
            //如果当前 bmap 没找到,继续往下一个 bmap 找,直到找完所有桶(包括溢出桶)
            ovf := b.overflow(t)
            if ovf == nil {
                break
            }
            b = ovf
        }
    
        // Did not find mapping for key. Allocate new cell & add entry.
    
        // If we hit the max load factor or we have too many overflow buckets,
        // and we're not already in the middle of growing, start growing.
        // 到此处说明未找到,需要插入一个新的 key/value
        // 如果负载因子超过阈值(6.5) 或者 溢出桶太多 noverflow >= (1 << (B & 15))
        if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
            // 对不同的扩容情况,执行具体的扩容策略(加倍扩容/等量扩容)
            hashGrow(t, h)
            //重新执行上述流程
            goto again // Growing the table invalidates everything, so try again
        }
    
        if inserti == nil {
            // all current buckets are full, allocate a new one.
            // 如果所有位置都是满的,则分配一个新的溢出桶,然后把 key/value 放到第一个位置
            newb := h.newoverflow(t, b)
            inserti = &newb.tophash[0]
            insertk = add(unsafe.Pointer(newb), dataOffset)
            elem = add(insertk, bucketCnt*uintptr(t.keysize))
        }
    
        // store new key/elem at insert position
        // 如果 key 是指针
        if t.indirectkey() {
            kmem := newobject(t.key)
            *(*unsafe.Pointer)(insertk) = kmem
            insertk = kmem
        }
        //如果 value 是指针
        if t.indirectelem() {
            vmem := newobject(t.elem)
            *(*unsafe.Pointer)(elem) = vmem
        }
        typedmemmove(t.key, insertk, key)
        // 把该 key hash值的高8位赋值,然后元素个数 +1
        *inserti = top
        h.count++
    
    done:
        //再次检测在此执行期间是否有别的 goroutine 对这个 map 执行了写操作
        if h.flags&hashWriting == 0 {
            throw("concurrent map writes")
        }
        //把 flags 中对应的 hashWriting 标志清空
        h.flags &^= hashWriting
        if t.indirectelem() {
            elem = *((*unsafe.Pointer)(elem))
        }
        return elem
    }
    map-set

    从上可以知道:map 在两种情况下会发生扩容,1:装载因子已经超过 6.5;2:哈希使用了太多的溢出桶(至于什么是 ”太多“, 则需要看相应扩容源码部分)

    map 的扩容

    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 + 1, 这种是加倍扩容的情景
        bigger := uint8(1)
        //如果不是负载因子的原因,则执行等量扩充
        if !overLoadFactor(h.count+1, h.B) {
            bigger = 0
            h.flags |= sameSizeGrow
        }
        //将当前的元素放入到 oldbuckets 中
        oldbuckets := h.buckets
        //新申请一个 bucket 数组,容量是 pow(2, B+(0/1))
        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)
        // 对 bmap 中的元素进行更新
        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().
    }
    map-grow

    等量扩容: 当溢出桶的个数太多时,执行等量扩容操作,原因是对这个 map 执行了大量的添加删除操作,但是 key 的个数并没有增多,负载因子也并没有达到阈值(6.5), 想象这样一种场景:在 map 中插入删除了大量的 key, 导致正正常 bucket 中的元素都是空的,但是溢出桶中的元素都是满的,这时,我们并不需要分配额外的空间,只需要重新生成一个等量的 bucket 数组,然后对上述 oldbuckets 中的元素进行重新 hash, 然后放入 buckets 就可以了(从新执行以下 hash 映射就可以)。

    参考资料

    【 Go 夜读 】 #44 Go map 源码阅读分析 

  • 相关阅读:
    构建可靠的系统
    netty详解之reactor模型
    netty详解之io模型
    小明的魔法调度框架之旅
    JAVA版-微信高清语音.speex转.wav格式
    Spring Data JPA 缓存结合Ehcache介绍
    @media print样式 关于table断页
    JBPM学习第6篇:通过Git导入项目
    JBPM学习第5篇:Mysql配置
    JBPM学习第4篇:10分钟熟悉Eclipse
  • 原文地址:https://www.cnblogs.com/zpcoding/p/13603182.html
Copyright © 2020-2023  润新知