分享
  1. 首页
  2. 文章

Golang Map 实现 (二) map 的创建

搬砖程序员带你飞 · · 4967 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本文在golang map 数据结构的基础上,学习一个make 是如何构造的。

map 创建示例

在golang 中,初始化一个map 算是有两种方式。

example1Map := make(map[int64]string)
example2Map := make(map[int64]string, 100)

第一种方式默认不指定map的容量,第二种会指定后续map的容量估计为100,希望在创建的时候把空间就分配好。

当make创建map时,底层做了什么

对于不同的初始化方式,会使用不同的方式。下面是提供的几种初始化方法:

// hint 就是 make 初始化map 的第二个参数
func makemap(t *maptype, hint int, h *hmap) *hmap
func makemap64(t *maptype, hint int64, h *hmap) *hmap
func makemap_small() *hmap

区别在于:
如果不指定 hint,就调用makemap_small;
如果make 第二个参数为int64, 则调用makemap64;
其他情况调用makemap方法。下面我们逐一学习。

makemap_small

func makemap_small() *hmap { 
 h := new(hmap)
 h.hash0 = fastrand()
 return h
}

fastrand 是创建一个seed,在生成hash值时使用。
所以在makemap_small 时,只是创建了一个hmap 的结构体,并没有初始化buckets.

makemap64

func makemap64(t *maptype, hint int64, h *hmap) *hmap {
 if int64(int(hint)) != hint {
 hint = 0
 }
 return makemap(t, int(hint), h)
}

makemap64 是对于传入的第二个参数为int64 的变量使用的。 如果hint的值大于int最大值,就将hint赋值为0,否则和makemap 初始化没有差别。为什么不把大于2^31 - 1 的map 直接初始化呢?因为在hmap 中 count 的值就是int,也就是说map最大就是 2^31 - 1 的大小

makemap

这个是初始化map的核心代码了,需要我们慢慢品味。

一开始,我们需要了解下maptype这个结构, maptype 标识一个map 数据类型的定义,当然还有其他的类型,比如说interfacetype,slicetype,chantype 等。maptype 的定义如下:

type maptype struct {
 typ _type // type 类型
 key *_type // key 的type
 elem *_type // value 的type
 bucket *_type // internal type representing a hash bucket
 keysize uint8 // key 的大小
 valuesize uint8 // value 的大小
 bucketsize uint16 // size of bucket
 flags uint32
}

maptype 里面存储了kv的对象类型,bucket类型,以及kv占用内存的大小。以及bucketsize的大小,还有一些标记字段(flags)。在map 实现时,需要用到这些字段做偏移计算等。

下面是 makemap 的代码:


// hint 需要创建的 map 大小(预计要添加多少元素)
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)
 }
 // xorshift64+ 算法, 可以研究下
 h.hash0 = fastrand()
 // 计算B 的值
 // 如果大于8,就先申请好。
 // 申请规则就是刚好满足 hint < 6.5 * 2 ^ B 的时候 (B 最大是63)
 // 其中6.5 相当于每个bucket 链表中,平均有6.5个bucket
 // 所以最长的map,应该是 6.5 * 2^63 (正常用肯定不会溢出)
 
 B := uint8(0)
 for overLoadFactor(hint, B) {
 B++
 }
 h.B = B
 // 接着数据初始化, 如果 容量小于等于8的,就在用的时候初始化, B 为0
 if h.B != 0 {
 var nextOverflow *bmap
 // 申请一个buckets 数组
 h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
 if nextOverflow != nil {
 h.extra = new(mapextra)
 h.extra.nextOverflow = nextOverflow
 }
 }
 return h
}

首先,通过bucketsize 和hint 的值,计算出需要分配的内存大小mem, 以及是否会overflow (大于指针的最大地址范围),如果溢出或者申请的内存大于最大可以申请的内存时,就设置hint为0了,直接不初始化buckets了。

接着,和makemap_small 一样,初始化一个随机的种子。

然后,计算B的值. 在overLoadfactor 中,判断了hint 的大小。如果小于等于8,那B就不再赋值,直接不初始化数据。如果B大于8,那就计算B了。这里涉及到一个填充因子的概念。大概意思就是说,每个hash值(也就是pos)中,平均放多少个kv数据,默认是6.5;所以判断标准就是hint 必须满足如下的条件:

hint < 6.5 * (1 << B)

通过增加B的值,直到上面的表达式满足为止。这样B就初始化好了。

最后,申请一个bucket数组,赋值给buckets,如果有多申请出来的buckets,那就赋值给extra.nextOverflow, 当溢出之后,从多申请出来的buckets 里面取(也是为了避免内存分配)。

下面就详细看下初始化一个buckets的构建。

makeBucketArray

makeBucketArray 用于初始化一个Bucket 数组。也就是hmap 中的buckets,下面是相关代码:

func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
 base := bucketShift(b)
 nbuckets := base
 // 为了防止溢出的迁移,加一点冗余的bucket
 if b >= 4 { 
 // ... 修改nbuckets
 }
 // 如果之前没有分配过,那直接分配
 if dirtyalloc == nil {
 buckets = newarray(t.bucket, int(nbuckets))
 } else {
 // 使用以前分配好的
 buckets = dirtyalloc
 size := t.bucket.size * nbuckets
 if t.bucket.kind&kindNoPointers == 0 {
 memclrHasPointers(buckets, size)
 } else {
 memclrNoHeapPointers(buckets, size)
 }
 }
 if base != nbuckets {
 // 处理多申请出来的bucket
 }
 return buckets, nextOverflow
}

这里用到了比较多的指针计算,需要细细品读。

  • 首先,就是就是通过B计算一个base值,base = 1 << B (2 ^ B)

nbuckets 是需要申请的数组的长度,正常情况下 base 值就是数组长度。但是,如果 base 大于16时,会预分配一些需要后期做overflow的bucket。这个overflow的计算规则如下:

 nbuckets += bucketShift(b - 4)
 sz := t.bucket.size * nbuckets
 up := roundupsize(sz)
 if up != sz {
 nbuckets = up / t.bucket.size
 }

在base 的基础上,多分配 base / 16 长度的bucket。然后根据内存的分配规则(包括了页大小和内存对齐等规则),计算出合适的分配内存的大小,然后计算出 bucket 的分配个数 nbuckets.

  • 其次,如果有之前未分配内存,那就初始化一个数组(终于等到了这一步),如过有dirtyalloc, 那就使用dirtyalloc 的内存(其实是用来清除map中数据使用的),然后把dirtyalloc中不需要的数据清除引用。
  • 最后,如果除了需要申请的base 长度的bucket外,还多申请了一些bucket,下面是对多申请的数据做的处理:
 // 上面添加了一些nbuckets 防止溢出,所以B 值取模就不太合理了,所以有一个mapextra 的数据节点
 // 数据分配也很有趣,从刚申请的buckets数组中,取出后面的一段分给mapextra
 // nextOverflow 分配给mapextra
 nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
 // 取nextOverflow 里面的最后一个元素,并把最后一个buckets 的末尾偏移的指针指向空闲的bucket (目前就是第一个buckets 了)
 last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
 last.setoverflow(t, (*bmap)(buckets))

先计算出多申请出来的内存地址 nextOverflow,然后计算出 申请的最后一块bucket的地址,然后将最后一块bucket的overflow指针(指向链表的指针)指向buckets 的首部。 原因呢,是为了将来判断是否还有空的bucket 可以让溢出的bucket空间使用。

今天的作业就交完了。下一篇将学习golang map的数据初始化实现。

参考

weixin_logo.png


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:搬砖程序员带你飞

查看原文:Golang Map 实现 (二) map 的创建

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
4967 次点击
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏