美文网首页
go 的 map 实现原理

go 的 map 实现原理

作者: wayyyy | 来源:发表于2022-04-25 00:16 被阅读0次

Go 语言的map 使用 hash 表作为底层实现,一个 Hash 表可以有多个 bucket,而每个 bucket 保存了 map 中的一个或一组键值对。

map的内存模型
map.jpg
type hmap struct {
    count      i nt               // 元素个数
    flags      uint8
    B          uint8             // buckets 数组的长度就是 2^B 
    hash0      uint32 
    buckets    unsafe.Pointer    // 指向 buckets 数组,大小为 2^B,如果元素个数为0,就为 nil
    oldbuckets unsafe.Pointer    // 老旧的 buckets 数据,用来扩容 
    nevacuate  uintptr       
    extra      *mapextra 
}

buckets 是一个指针,最终它指向的是一个结构体:

type bmap struct {
    tophash [bucketCnt]uint8
}

但这只是表面(src/runtime/hashmap.go)的结构,编译期间会给它加料,动态地创建一个新的结构:

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr    // 溢出buckets
}

bmap 就是我们常说的 bucket,元素经过 hash 运算后就会落到某个 bucket 中存储。

image.png

注意到 key-value 不是 ke/value key/value 这样放在一起的,是 key/key/.../value/value/的形式,源码解释是为了可以省掉paddding,节省内存空间。

hash 冲突
  • 解决 hash 冲突
    当有两个或以上数量的健被 hash 到同一个 bucket 时,就发生了键冲突,Go 用链地址法来解决键冲突,由于每一个bucekt 可以存放8个键值对,所以同一个 bucket 存放超过 8个键值对时就会再创建一个 bucket,用类似的链表的方式将 bucket 连接起来。

    image.png

    在bucket 的数据结构中使用指针指向溢出的 bucket,意为当前的 bucket 盛不下而溢出的部分,这并不是一件好事,因为它降低了存取效率。

  • 负载因子
    负载因子用于衡量一个 Hash 表冲突的情况,公式为:

    负载因子 = 键数 / bucket 数量

    对于一个 bucket 数量为4,包含4个键值对的 Hash 表来说,这个 hash 表的负载因子为1,负载因子过小或者过大都不理想:

    • 负载因子过小,说明空间利用率低。
    • 负载因子过大,说明冲突严重,存取效率低。
      当 hash 表负载因子过大时,需要申请更多的 bucket,并对所有键值对重新组织,使其均匀分布到这些 bucket 中,这个过程称为 rehash。每个 hash 表的实现对负载因子容忍程度不同,比如 Redis 的map 实现中负载因子为1 就会触发rehash,而 go 是 6.5,因为 Redis 每个 bucket 只能存1个键值对,而 go 的bucket 可以存 8个键值对。
扩容
  • 扩容条件
    降低负载因子常用的手段是扩容,为了保证访问效率,当新元素将要添加进 map 时。都会检查是否需要扩容,扩容实际上是以空间换时间的手段。
    触发扩容需要满足下列的条件:

    • 负载因子大于6.5时,即平均每个 bucket 存储的键值对达到 6.5 个以上。
    • overflow 的数量达到 2^min(15, B) 时。
  • 增量扩容
    当负载因子过大时,就新建一个 bucket 数组,新的 bucket 数组的长度是原来的2倍,然后旧的 bucket 数组中的数据搬迁到新的 bucket 数组中。
    考虑到如果 map 存储了数以亿计的键值对,那么一次性搬迁会造成比较大的延时,所以 Go 采用逐渐搬迁策略,即每次访问 map 时都会触发一次搬迁,每次搬迁2个键值对。
    比如现在有一map,存储了 7个键值对,只有1个bucket,此时负载因子为7,再次添加时会触发扩容操作,扩容之后再将新的键值对写入新的 bucket 中。当添加第8个键值对时,将触发扩容:

    image.png

    扩容时,先让hmap中的oldbuckets指向原buckets数组,然后申请新的buckets数组,并将数组指针保存到 hmap数据结构的buckets成员中,这样就完成了新老buckets数组的交接,后续的迁移工作将是从oldbuckets数组逐步搬迁键值对到新的buckets数组中,待oldbuckets数组中的所有键值对搬迁完毕后,就可以安全的释放 oldbuckets数组。

  • 等量扩容
    等量扩容,并不是扩大容量,而是 bucket 数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使 bucket 的使用率更高,进而保证更快的存取效率。

key 的查找

查找过程如下:

  • 根据 key 值 计算 hash 值
  • 取 hash 值低位 与 hmap.B 取模来确定 bucket 的值
  • 取 hash 值高位,在 tophash 数组中查询
  • 如果 tophash[i] 中存储的 hash值 与当前的 key 的 hash 值相等,则获取 tophash[i] 的 key 值进行比较
  • 当前bucket 中没有找到,则从溢出的bucket继续查找

如果当前 map 处于搬迁过程中,那么查找时优先从 oldbuckets 数组中查找。然后再从新的buckets 数组中查找。

key 的添加

新元素添加过程如下:

  • 根据 key 的值计算 hash 值
  • 取 hash 值低位 与 hmap.B 取模来确定 bucket 的值
  • 查找该 key 是否已经存在,如果存在则直接更新值
  • 如果该 key 不存在,则从该 bucket 中寻找空余位置并插入

如果当前 map 处于搬迁过程中,那么新元素会直接添加到新的buckets 数组中。

key 的删除

删除元素实际上先查找元素,如果元素存在则把元素从相应的 bucket 中清除,如果不存在则什么都不做。

相关文章

  • Golang之Map源码

    引用 深入 Go 的 Map 使用和实现原理 哈希表 深度解密Go语言之map Golang map 的底层实现 使用

  • go map 数据结构

    Go map实现原理[https://my.oschina.net/renhc/blog/2208417?noca...

  • go 的 map 实现原理

    Go 语言的map 使用 hash 表作为底层实现,一个 Hash 表可以有多个 bucket,而每个 bucke...

  • 2018-09-09

    语言基础(go)及生态 gorutine实现原理 gc channel 上下文管理器 map的底层实现,如何保证线...

  • GO 中 map 的实现原理

    GO 中 map 的实现原理 嗨,我是小魔童哪吒,我们来回顾一下上一次分享的内容 分享了切片是什么 切片和数组的区...

  • hash map 实现原理

    HashMap 的存取实现 1.1 put refrencehash map实现hash map实现原理

  • 深入之数组方法的实现原理

    map的用法和实现原理: map即映射,将原数组映射返回新数组。 用法: 实现原理: filter的用法和实现原理...

  • 第九章:Go语言映射类型map

    1. map概述 Go语言中map字典类型时散列表(hash table)的实现,因为Go语言中将map中的每个键...

  • 解析js数组中的map,filter, reduce, find

    map的用法和实现原理 用法 "map"即"映射",也就是原数组被"映射"成对应新数组。 实现原理 filter的...

  • 10.map

    Go语言基础之map | Golang Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。 ...

网友评论

      本文标题:go 的 map 实现原理

      本文链接:https://www.haomeiwen.com/subject/ffqzertx.html