zoukankan      html  css  js  c++  java
  • Golang 中如何优雅的使用map?

    Golang中,通过哈希查找实现hash,通过链表解决hash冲突

    map的内存模型

    type hmap struct {
        count     int // map 中的元素个数,必须放在 struct 的第一个位置,因为 内置的 len 函数会从这里读取
        flags     uint8
        B         uint8  // log_2 of # of buckets (最多可以放 loadFactor * 2^B 个元素,再多就要 hashGrow 了)
        noverflow uint16 // overflow 的 bucket 的近似数
        hash0     uint32 // hash 种子,为hash函数结果引入随机性
    
        buckets    unsafe.Pointer // 2^B 大小的数组,如果 count == 0 的话,可能是 nil
        oldbuckets unsafe.Pointer // 一半大小的之前的 bucket 数组,只有在扩容过程中是非 nil
        nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
    
        extra *mapextra           // 当 key 和 value 都可以 inline 的时候,就会用这个字段
    }

    map中更小的单元桶,每一个桶会装8个key,通过hash结果的高8位决定在桶里具体的位置,由hash结果的低B位决定落在哪个桶

    bmap内存结构

    // bucket 本体
    type bmap struct {
        // tophash 是 hash 值的高 8 位
        tophash [bucketCnt]uint8
        // keys
        // values
        // overflow pointer
    }

    bmap是存具体key-value的地方,进一步观察bmap底层, 将key,value分开存储可以避免在key、value不是同一种结构时出现的内存碎片,比如map[int64]int8,在每一个key-value后面都要padding7个字节,分开存储就只需要在最后加padding

    当一个bmap满了以后,会通过overflow连接一个溢出桶,实际会将每一个bmap中的overflow统一迁移到hmap的extra中,主要是为了避免gc时扫描整个hmap,所以不在单独的桶里设置指针,而是直接让其指向hmap.extra.overflow数组

    mapextra底层结构

    type mapextra struct {
        // 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
        // 使用 extra 来存储 overflow bucket,这样可以避免 GC 扫描整个 map
        // 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针
        // 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了
        // overflow 包含的是 hmap.buckets 的 overflow 的 bucket
        // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket
        overflow    *[]*bmap
        oldoverflow *[]*bmap
        // 指向空闲的 overflow bucket 的指针
        nextOverflow *bmap
    }

    map的初始化

    hashTable := make(map[k]v, hint),当hint >= 4时后面会追加2^(hint-4)个桶,之后再内存页帧对齐又追加了若干个桶

    // make(map[k]v, hint), hint即预分配大小
    // 不传hint时,如用new创建个预设容量为0的map时,makemap只初始化hmap结构,不分配hash数组
    func makemap(t *maptype, hint int, h *hmap) *hmap {
        // 省略部分代码
        // 随机hash种子
        h.hash0 = fastrand()
    
        // 2^h.B 为大于hint*6.5(扩容因子)的最小的2的幂
        B := uint8(0)
        // overLoadFactor(hint, B)只有一行代码:return hint > bucketCnt && uintptr(hint) > loadFactorNum*(bucketShift(B)/loadFactorDen)
        // 即B的大小应满足 hint <= (2^B) * 6.5
        // 一个桶能存8对key-value,所以这就表示B的初始值是保证这个map不需要扩容即可存下hint个元素对的最小的B值
        for overLoadFactor(hint, B) {
            B++
        }
        h.B = B
    
        // 这里分配hash数组
        if h.B != 0 {
            var nextOverflow *bmap
            h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
            // makeBucketArray()会在hash数组后面预分配一些溢出桶,
            // h.extra.nextOverflow用来保存上述溢出桶的首地址
            if nextOverflow != nil {
                h.extra = new(mapextra)
                h.extra.nextOverflow = nextOverflow
            }
        }
    
        return h
    }

    分配hash数组

    // 分配hash数组
    func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
        base := bucketShift(b) // base代表用户预期的桶的数量,即hash数组的真实大小
        nbuckets := base // nbuckets表示实际分配的桶的数量,>= base,这就可能会追加一些溢出桶作为溢出的预留
        if b >= 4 {
            // 这里追加一定数量的桶,并做内存对齐
            nbuckets += bucketShift(b - 4)
            sz := t.bucket.size * nbuckets
            up := roundupsize(sz)
            if up != sz {
                nbuckets = up / t.bucket.size
            }
        }
    
        // 后面的代码就是申请内存空间了,此处省略
        // 这里大家可以思考下这个数组空间要怎么分配,其实就是n*sizeof(桶),所以:
            // 每个桶前面是8字节的tophash数组,然后是8个key,再是8个value,最后放一个溢出指针
            // sizeof(桶) = 8 + 8*sizeof(key) + 8*sizeof(value) + 8
        
        return buckets, nextOverflow
    }

    map的读写

    value, ok := map[key]  #可以通过ok判断map中是否存在这个key

    valuePtr := map[key]   #返回value对应类型的空值,对于key和value在map中存储的是否为指针,需要根据key,value的大小来判断,当大于128字节时,key和value存储的都为指针

    将map作为函数参数时,会传递一个指针副本,对该副本操作,也同样会影响原始map对象中的值

    对于访问key,通过key低B位确定哪个桶,通过桶高8位去与桶里每个tophash比较,只查找时,比较未找到就继续去overflow溢出桶里找,如果是插入没有找到,就插入第一个hashtop为empty的位置

    map的key可以是除了slice,map,func 的任意类型,value可以是任意类型。

    map并不是一个线程安全的数据结构,在边遍历边删除是一个同时读写的行为,被检测到会直接报panic

    map的删除

    通过delete(map, key)函数删除key,相应的hashtop被置为empty,并没有立即释放内存,在指针没有引用时会被系统gc

    map扩容,扩容的两个临界点

    1.由装载因子确定,默认6.5 、即元素个数 > = 桶个数 * 6.5,表示大部分桶已满 (B+1,将hmap的bucket数组扩容一倍)

    2.由overflow的桶个数决定,当overflow溢出桶太多,代表可能是一边插入一边删除,导致大量桶出现空洞,此时值存储的非常稀疏,当bucket总数<2^15 时,overflow的bucket总数>=bucket总数;

    bucket总数>=2^15,overflow的bucket >= 2^15,即认为溢出桶太多(移动bucket内容,使其更加紧密进而提高bucket利用率)缩容

    当map中拥有大量hash冲突,也可能会导致overflow溢出桶数量过多,但这只是一种理论可能,golang中map中的hash随机种子几乎可以避免这种情况。

    扩容过程:

    由hmap内存结构知道了,buckets指向新的内存地址,oldbuckets仍然指向老的内存地址,golang中map的扩容是一种渐进式扩容,原有key/value不会一次性全部迁移完毕,每次只会迁移2个buckets,每一次搬迁以buckets作为单元,包括hash桶和这个桶的溢出链表。

    对于2的缩容并不是真正的缩容,hash的容量没有发生变化,只是让数据更加紧凑,如果要做到真正的缩容就需要重新创建一个map,再复制。

    参考:

    1.https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/

    2.https://segmentfault.com/a/1190000023879178

    3.https://www.cnblogs.com/JoZSM/p/11784037.html

    4.https://github.com/cch123/golang-notes/blob/master/map.md

  • 相关阅读:
    修改tableVIewCell里控件的属性
    常用正则表达式
    解决定时器在主线程不工作问题
    iOS App集成Apple Pay
    使用UIDataDetectorTypes自动检测电话、网址和邮箱
    NSDictionary初始化的坑
    关于UIViewController的presentViewController实现
    iOS8以上使用CoreLocation定位
    .xib 创建的文件 [self.view bringSubviewToFront:XXX];不好用
    socket通讯----GCDAsyncSocke 解读
  • 原文地址:https://www.cnblogs.com/peterleee/p/14063766.html
Copyright © 2011-2022 走看看