本文学习 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。这里面要注意以下几点:

  1. 为啥是2的幂次方 为了做完hash后,通过掩码的方式取到数组的偏移量, 省掉了不必要的计算。
  2. B 这个数是怎么确定的 这个和我们map中要存放的数据量是有很大关系的。我们在创建map的时候来详述。
  3. 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位)

  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. 面试刷题30:SpringBean的生命周期?

    spring是Java软件开发的事实标准. 我是李福春,我在准备面试,今天的问题是:springBean的生命周期是怎样的? 答:spring最基础的能力是IOC(依赖注入),AOP(面向切面编程), ...

  2. Javascript/Jquery遇到字符串自动NaN的问题

    结果发现是一个id绑定了两次点击事件.修改之后,问题解决.

  3. jdk1.8练习201909200843By:镜花水月(月神)

    package org.jimmy.test2019091701; import java.util.Arrays; import java.util.List; import java.util.S ...

  4. 在docker中部署redis主从配置

    环境说明: 阿里云服务器 Ubuntu 16.04 docker 1.拉取Redis镜像 docker pull redis 2.配置Redis启动配置文件,此处我创建一个专用目录,存放Redis相关 ...

  5. 【高并发】什么是ForkJoin?看这一篇就够了!

    写在前面 在JDK中,提供了这样一种功能:它能够将复杂的逻辑拆分成一个个简单的逻辑来并行执行,待每个并行执行的逻辑执行完成后,再将各个结果进行汇总,得出最终的结果数据.有点像Hadoop中的MapRe ...

  6. qt creator源码全方面分析(3-9)

    依赖分析图 我们对库和插件的依赖性进行分析,并画图如下,稍微省略了一些插件,画出来太乱了,核心的都在图中了. 原创造福大家,共享改变世界 献出一片爱心,温暖作者心灵

  7. 原理解密 → Spring AOP 实现动态数据源(读写分离),底层原理是什么

    开心一刻 女孩睡醒玩手机,收到男孩发来一条信息:我要去跟我喜欢的人表白了! 女孩的心猛的一痛,回了条信息:去吧,祝你好运! 男孩回了句:但是我没有勇气说不来,怕被打! 女孩:没事的,我相信你!此时女孩 ...

  8. yii2框架学习笔记

    1.去掉yii2模版默认的头部和脚部的两种方法: (1) 第一种 $this->layout = false; $this->render('index'); (2) 第二种(partia ...

  9. [一起面试AI]NO.5过拟合、欠拟合与正则化是什么?

    Q1 过拟合与欠拟合的区别是什么,什么是正则化 欠拟合指的是模型不能够再训练集上获得足够低的「训练误差」,往往由于特征维度过少,导致拟合的函数无法满足训练集,导致误差较大. 过拟合指的是模型训练误差与 ...

  10. Linux网络安全篇,进入SELinux的世界(二)

    一.简单的网页制作 1.启动httpd服务 /etc/init.d/httpd start 2.编写首页网页文件 echo "hello,this is my first webPage&q ...