本文学习 Golang 的 Map 数据结构,以及map buckets 的数据组织结构。

hash 表是什么

从大学的课本里面,我们学到:hash 表其实就是将key 通过hash算法映射到数组的某个位置,然后把对应的val存放起来。

如果出现了hash冲突(也就是说,不同的key被映射到了相同的位置上时),就需要解决hash冲突。解决hash冲突的方法还是比较多的,比如说开放定址法,再哈希法,链地址法,公共溢出区等(复习下大学的基本知识)。

其中链地址法比较常见,下面是一个链地址法的常见模式:

Position 指通过Key 计算出的数组偏移量。例如当 Position = 6 的位置已经填满KV后,再次插入一条相同Position的数据将通过链表的方式插入到该条位置之后。

在php的Array 中是这么实现的,golang中也基本是这么实现。下面我们学习下Golang中map的实现。

Golang Map 实现的数据结构

Golang的map中,首先把kv 分在了N个桶中,每个桶中的数据有8条(bucketCnt)。如果一个桶满了(overflow),也会采用链地址法解决hash 的冲突。

下面是定义一个hashmap的结构体:

  1. type hmap struct {
  2. // 长度
  3. count int
  4. // map 的标识, 下方做了定义
  5. flags uint8
  6. // 实际buckets 的长度为 2 ^ B
  7. B uint8
  8. // 从bucket中溢出的数量,(存在extra 里面)
  9. noverflow uint16
  10. // hash 种子,做key 哈希的时候会用到
  11. hash0 uint32
  12. // 存储 buckets 的地方
  13. buckets unsafe.Pointer
  14. // 迁移时oldbuckets中存放部分buckets 的数据
  15. oldbuckets unsafe.Pointer
  16. // 迁移的数量
  17. nevacuate uintptr
  18. // 一些额外的字段,在做溢出处理以及数据增长的时候会用到
  19. extra *mapextra
  20. }
  21. const (
  22. // 有一个迭代器在使用buckets
  23. iterator = 1
  24. // 有一个迭代器在使用oldbuckets
  25. oldIterator = 2
  26. // 并发写,通过这个标识报panic
  27. hashWriting = 4
  28. sameSizeGrow = 8
  29. )
  30. type mapextra struct {
  31. overflow *[]*bmap
  32. oldoverflow *[]*bmap
  33. nextOverflow *bmap
  34. }
  35. type bmap struct {
  36. tophash [bucketCnt]uint8
  37. }

表中除了对基本的hash数据结构做了定义外,还对数据迁移、扩容等操作做了定义,这里我们可以忽略,等学习到时我们再深入了解。

深入 桶列表 (buckets)

buckets 字段中是存储桶数据的地方。正常会一次申请至少2^N长度的数组,数组中每个元素就是一个桶。N 就是结构体中的B。这里面要注意以下几点:

  1. 为啥是2的幂次方 为了做完hash后,通过掩码的方式取到数组的偏移量, 省掉了不必要的计算。
  2. B 这个数是怎么确定的 这个和我们map中要存放的数据量是有很大关系的。我们在创建map的时候来详述。
  3. bucket 的偏移是怎么计算的 hash 方法有多个,在 runtime/alg.go 里面定义了。不同的类型用不同的hash算法。算出来是一个uint32的一个hash 码,通过和B取掩码,就找到了bucket的偏移了。下面是取对应bucket的例子:
  1. // 根据key的类型取相应的hash算法
  2. alg := t.key.alg
  3. hash := alg.hash(key, uintptr(h.hash0))
  4. // 根据B拿到一个掩码
  5. m := bucketMask(h.B)
  6. // 通过掩码以及hash指,计算偏移得到一个bucket
  7. b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))

深入 桶 (bucket)

一个桶的示意图如下:

每个桶里面,可以放8个k,8个v,还有一个overflow指针(就是上面的next),用来指向下一个bucket 的地址。在每个bucket的头部,还会放置一个tophash,也就是bmap 结构体。这个数组里面存放的是key的hash值,用来对比我们key生成的hash和存出的hash是否一致(当然除了这个还有其他的用途,后面讲数据访问的时候会讲到)。 tophash中的数据,是从计算的hash值里面截取的。获取bucket 是用的低bit位的hash,tophash 使用的是高bit位的hash值(8位)

  1. 为啥bucket 一次要存8个kv,而不是一个kv放一个bucket,然后链地址法做处理就OK了 据我分析,有几点原因: a, 一次分配8个kv的空间,可以减少内存的分配频次; b,减少了overflow指针的内存占用,比如说8个kv,采用一个一个存储的话,需要8 * 8B (64位机) = 64B的数据存下一个的地址,而采用go实现的这种方式,只需要 8B + 8B (bmap的大小) = 16B 的数据就可以了。
  2. 为啥需要用tophash 一般的hash 实现逻辑是直接和key比较,如果比较成功,这找到相应key的数据。但是这里用到了tophash,好处是可以减少key的比较成本(毕竟key 不一定都是整数形式存在的)
  3. 为啥是8个 8 * 8B = 64B 整好是64位机的一个最小寻址空间,不过可以通过修改源码自定义吧。
  4. 为什么key 和val 要分开放 这个也比较好理解,key 和val 都是用户可以自定义的。如果key是定长的(比如是数字,或者 指针之类的,大概率是这样。)内存是比较整齐的,利于寻址吧。

技术总结

golang 实现的map比朴素的hashmap 在很多方面都有优化。

  1. 使用掩码方式获取偏移,减少判断。
  2. bucket 存储方式的优化。
  3. 通过tophash 先进行一次比较,减少key 比较的成本。
  4. 当然,有一点是不太明白的,为啥 overflow 指针要放在 kv 后面? 放在tophash 之后的位置岂不是更完美?

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

参考

[1] 深入理解 Go map:初始化和访问

Golang Map实现(一)的更多相关文章

  1. 【GoLang】GoLang map 非线程安全 & 并发度写优化

    Catena (时序存储引擎)中有一个函数的实现备受争议,它从 map 中根据指定的 name 获取一个 metricSource.每一次插入操作都会至少调用一次这个函数,现实场景中该函数调用更是频繁 ...

  2. golang map输出排序

    由于GoLang Map 内部存储是无序的,当需要按顺序获得map存储的key -value值时,应该对遍历出来的结果进行重新排序: 在go 1.8版本后,提供的slice sort 功能使排序更简单 ...

  3. Golang Map实现(四) map 的赋值和扩容

    title: Golang Map 实现 (四) date: 2020-04-28 18:20:30 tags: golang map 操作,是map 实现中较复杂的逻辑.因为当赋值时,为了减少has ...

  4. golang map getkeys

    golang 获取map的keys package main import "fmt" import "reflect" func main() { abc : ...

  5. golang map

    Our friend Monk has been made teacher for the day today by his school professors . He is going to te ...

  6. golang map 读写锁与深度拷贝的坑

    0X01 golang中,map(字典)无法并发读写 简单来说,新建万条线程对同一个map又读又写,会报错. 为此,最好加锁,其实性能影响并不明显. type taskCache struct{ sy ...

  7. Golang map 如何进行删除操作?

    Cyeam 关注 2017.11.02 10:02* 字数 372 阅读 2784评论 0喜欢 3 map 的删除操作 Golang 内置了哈希表,总体上是使用哈希链表实现的,如果出现哈希冲突,就把冲 ...

  8. Golang map并发 读写锁

    golang并发 一:只有写操作 var ( count int l = sync.Mutex{} m = make(map[int]int) ) //全局变量并发写 导致计数错误 func vari ...

  9. Golang Map Addressability

    http://wangzhezhe.github.io/blog/2016/01/22/golangmapaddressability-dot-md/ 在golang中关于map可达性的问题(addr ...

随机推荐

  1. React源码解析——创建更新过程

    一.ReactDOM.render 创建ReactRoot,并且根据情况调用root.legacy_renderSubtreeIntoContainer或者root.render,前者是遗留的 API ...

  2. Git入门操作(一)

    最近真正用到了Git,感觉还是需要好好整理一下最最基础用法,与萌新共享.^_^ 关于Git的基础介绍,这里不再赘述,下面撸代码了(主要是命令行的操作,属于linux操作系统的,可能没听过,但记住就好了 ...

  3. FZU - 2204 简单环形dp

    FZU - 2204 简单环形dp 题目链接 n个有标号的球围成一个圈.每个球有两种颜色可以选择黑或白染色.问有多少种方案使得没有出现连续白球7个或连续黑球7个. 输入 第一行有多组数据.第一行T表示 ...

  4. background-clip 和 background-origin 有什么区别? -[CSS] - [属性]

    这两个属性在W3S上的示例,给人的感觉好像效果是一样的:

  5. vue使用axios发送post请求时的坑及解决原理

    前言:在做项目的时候正好同事碰到了这个问题,问为什么用axios在发送请求的时候没有成功,请求不到数据,反而是报错了,下图就是报错请求本尊 vue里代码如下: this.$http.post('/ge ...

  6. Win 10 C 盘突然爆满,怎么清理?

    Win 10 C 盘突然爆满,怎么清理? 使用windows的小伙伴们都知道,C盘是安装系统的,有时候不知道为啥突然就爆满了,查看我的电脑,C盘显示红色的.是不是感觉狠揪心,想删除一些东西有不敢删除, ...

  7. Spring(二):初始值赋值

    依赖注入 1.构造器注入 见spring简介最后一大点. 2.Set方式注入 依赖注入:Set注入 依赖:bean对象的创建依赖于容器. 注入:bean对象的所有属性,由容器注入. bean ,里面属 ...

  8. 100 Path Sum

    Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all ...

  9. Linux基础管理篇,软件管理程序,yum与rpm

    一.RPM 一般来说,rpm类型的文件在安装的时候,会检测当前的系统是否满足当前软件需要的环境.若符合,则该软件就会被安装,并且会把软件的相关信息写入/var/lib/rpm/目录下的数据库文件中. ...

  10. 汇编 RET 和 CALL

    https://blog.csdn.net/u013018721/article/details/51264199 1.我们先来实践一下 ret 指令 DATA SEGMENT A DB 12H B ...