Golang Map实现(一)
本文学习 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的结构体:
type hmap struct {
// 长度
count int
// map 的标识, 下方做了定义
flags uint8
// 实际buckets 的长度为 2 ^ B
B uint8
// 从bucket中溢出的数量,(存在extra 里面)
noverflow uint16
// hash 种子,做key 哈希的时候会用到
hash0 uint32
// 存储 buckets 的地方
buckets unsafe.Pointer
// 迁移时oldbuckets中存放部分buckets 的数据
oldbuckets unsafe.Pointer
// 迁移的数量
nevacuate uintptr
// 一些额外的字段,在做溢出处理以及数据增长的时候会用到
extra *mapextra
}
const (
// 有一个迭代器在使用buckets
iterator = 1
// 有一个迭代器在使用oldbuckets
oldIterator = 2
// 并发写,通过这个标识报panic
hashWriting = 4
sameSizeGrow = 8
)
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
type bmap struct {
tophash [bucketCnt]uint8
}
表中除了对基本的hash数据结构做了定义外,还对数据迁移、扩容等操作做了定义,这里我们可以忽略,等学习到时我们再深入了解。
深入 桶列表 (buckets)
buckets 字段中是存储桶数据的地方。正常会一次申请至少2^N长度的数组,数组中每个元素就是一个桶。N 就是结构体中的B。这里面要注意以下几点:
- 为啥是2的幂次方 为了做完hash后,通过掩码的方式取到数组的偏移量, 省掉了不必要的计算。
- B 这个数是怎么确定的 这个和我们map中要存放的数据量是有很大关系的。我们在创建map的时候来详述。
- bucket 的偏移是怎么计算的 hash 方法有多个,在 runtime/alg.go 里面定义了。不同的类型用不同的hash算法。算出来是一个uint32的一个hash 码,通过和B取掩码,就找到了bucket的偏移了。下面是取对应bucket的例子:
// 根据key的类型取相应的hash算法
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0))
// 根据B拿到一个掩码
m := bucketMask(h.B)
// 通过掩码以及hash指,计算偏移得到一个bucket
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位)
- 为啥bucket 一次要存8个kv,而不是一个kv放一个bucket,然后链地址法做处理就OK了 据我分析,有几点原因: a, 一次分配8个kv的空间,可以减少内存的分配频次; b,减少了overflow指针的内存占用,比如说8个kv,采用一个一个存储的话,需要8 * 8B (64位机) = 64B的数据存下一个的地址,而采用go实现的这种方式,只需要 8B + 8B (bmap的大小) = 16B 的数据就可以了。
- 为啥需要用tophash 一般的hash 实现逻辑是直接和key比较,如果比较成功,这找到相应key的数据。但是这里用到了tophash,好处是可以减少key的比较成本(毕竟key 不一定都是整数形式存在的)
- 为啥是8个 8 * 8B = 64B 整好是64位机的一个最小寻址空间,不过可以通过修改源码自定义吧。
- 为什么key 和val 要分开放 这个也比较好理解,key 和val 都是用户可以自定义的。如果key是定长的(比如是数字,或者 指针之类的,大概率是这样。)内存是比较整齐的,利于寻址吧。
技术总结
golang 实现的map比朴素的hashmap 在很多方面都有优化。
- 使用掩码方式获取偏移,减少判断。
- bucket 存储方式的优化。
- 通过tophash 先进行一次比较,减少key 比较的成本。
- 当然,有一点是不太明白的,为啥 overflow 指针要放在 kv 后面? 放在tophash 之后的位置岂不是更完美?
今天的作业就交完了。下一篇将学习golang map的数据初始化实现。
参考
Golang Map实现(一)的更多相关文章
- 【GoLang】GoLang map 非线程安全 & 并发度写优化
Catena (时序存储引擎)中有一个函数的实现备受争议,它从 map 中根据指定的 name 获取一个 metricSource.每一次插入操作都会至少调用一次这个函数,现实场景中该函数调用更是频繁 ...
- golang map输出排序
由于GoLang Map 内部存储是无序的,当需要按顺序获得map存储的key -value值时,应该对遍历出来的结果进行重新排序: 在go 1.8版本后,提供的slice sort 功能使排序更简单 ...
- Golang Map实现(四) map 的赋值和扩容
title: Golang Map 实现 (四) date: 2020-04-28 18:20:30 tags: golang map 操作,是map 实现中较复杂的逻辑.因为当赋值时,为了减少has ...
- golang map getkeys
golang 获取map的keys package main import "fmt" import "reflect" func main() { abc : ...
- golang map
Our friend Monk has been made teacher for the day today by his school professors . He is going to te ...
- golang map 读写锁与深度拷贝的坑
0X01 golang中,map(字典)无法并发读写 简单来说,新建万条线程对同一个map又读又写,会报错. 为此,最好加锁,其实性能影响并不明显. type taskCache struct{ sy ...
- Golang map 如何进行删除操作?
Cyeam 关注 2017.11.02 10:02* 字数 372 阅读 2784评论 0喜欢 3 map 的删除操作 Golang 内置了哈希表,总体上是使用哈希链表实现的,如果出现哈希冲突,就把冲 ...
- Golang map并发 读写锁
golang并发 一:只有写操作 var ( count int l = sync.Mutex{} m = make(map[int]int) ) //全局变量并发写 导致计数错误 func vari ...
- Golang Map Addressability
http://wangzhezhe.github.io/blog/2016/01/22/golangmapaddressability-dot-md/ 在golang中关于map可达性的问题(addr ...
随机推荐
- Docker笔记(十三):容器日志采集实践
日志是服务运行过程中的一个关键环节,借助日志,我们可以排查定位问题,也可以借助集中化的日志管理平台(如ELK)来做一些必要的数据统计分析.在Docker环境中,日志的采集比传统环境更为复杂,因此了解D ...
- JS获取HTML DOM元素的方法
JS获取HTML DOM元素有八种方法: 1.根据id获取标签元素: document.getElementById("id名"); 2.根据标签名获取标签列表: document ...
- 性能优化之三:将Dottrace过程加入持续集成
之前分享过一篇如何做接口性能分析的文章,但是整个分析过程有点繁琐,需要写一个控制台程序调用被测接口,再预热.启动dottrace追踪,最后才能得到我们想要的性能分析报告.如果有办法一键生成性能分析报告 ...
- G 树的难题
时间限制 : 10000 MS 空间限制 : 165536 KB 评测说明 : 1s,128m 问题描述 给出一个无根树.树有N个点,边有权值.每个点都有颜色,是黑色.白色.灰色这三种颜色之一,称 ...
- K - 回转寿司(值域段数(板题) + 动态开点)
回转寿司 Description 酷爱日料的小Z经常光顾学校东门外的回转寿司店.在这里,一盘盘寿司通过传送带依次呈现在小Z眼前.不同的寿 司带给小Z的味觉感受是不一样的,我们定义小Z对每盘寿司都有一个 ...
- NonRegisteringDriver造成的内存频繁FullGc
某天上服务器看了下gc情况,发现状况不对,启动了才2天的服务器发生了360次fullgc,这个频率肯定高了 说明 S0C.S1C.S0U.S1U:Survivor 0/1区容量(Capacity)和使 ...
- CH5E07 划分大理石(背包dp+二进制拆分)
传送门 大意: 有价值分别为1..6的大理石各a[1..6]块,现要将它们分成两部分,使得两部分价值之和相等,问是否可以实现.其中大理石的总数不超过20000. 解题思路: 妥妥的多重背包+二 ...
- 判断一组checkbox/redio是否被选中,为其添加样式
业务场景:当一行中有一个CheckBox被选中,则为此行添加class. <script type="text/javascript"> $(function(){ $ ...
- 记一次Windows蓝屏分析
大半夜收到此类信息,应该是让所有系统管理员最头大的事情了 首先我快速通过iDRAC,发现服务器发生了重启操作,并得到相关日志信息 通过Dell的官方解释,确定了该问题是OS层面的异常导致.打开Wind ...
- spring singleton实例中的变量怎么保证线程安全
pring中管理的bean实例默认情况下是单例的[sigleton类型],就还有prototype类型按其作用域来讲有sigleton,prototype,request,session,global ...