【Go语言 map源码分析】

map底层数据结构

我们在之前学习C++中的map时知道了 map的底层其实是有两种数据结构 这取决于我们要求它有序还是无序

  • 如果说我们要求map是有序的它的底层数据结构就是红黑树
  • 如果说我们要求map是无序的它的底层数据结构就是哈希表

但是Go语言中的map数据结构有点特殊 如下图

  • 当我们创建一个map对象的时候 实际上就是创建一个指针指向hmap结构体
  • 每个hmap结构体中包含若干个bucket
  • 每个bucket都是一个指向bmap结构体对象的指针
  • 每个bmap最多只能存放八个键值对 当到第九个的时候便会通过overflow指向下一个bmap

在这里插入图片描述

其中hmap 和 bmap的go语言源码如下

type hmap struct {
    count     int 
    // 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
    flags     uint8 
    // 状态标志(是否处于正在写入的状态等)
    B         uint8  
    // buckets(桶)的对数
    // 如果B=5,则buckets数组的长度 = 2^B=32,意味着有32个桶
    noverflow uint16 
    // 溢出桶的数量
    hash0     uint32 
    // 生成hash的随机数种子
    buckets    unsafe.Pointer 
    // 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil。
    oldbuckets unsafe.Pointer 
    // 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2;非扩容状态下,它为nil。
    nevacuate  uintptr        
    // 表示扩容进度,小于此地址的buckets代表已搬迁完成。
    extra *mapextra 
    // 存储溢出桶,这个字段是为了优化GC扫描而设计的
 }
//每个bucket是一个bmap结构体,bmap 结构体如下
type bmap struct{
    tophash [8]uint8
    keys [8]keytype 
    // keytype 由编译器编译时候确定
    values [8]elemtype 
    // elemtype 由编译器编译时候确定
    overflow uintptr 
    // overflow指向下一个bmap,overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}

插入元素的步骤

当我们向map中存储一个键值对的时候 会经过下面的步骤

在这里插入图片描述

  1. 通过k的哈希值和buckets长度取余 定位到key在哪一个bucket中
  2. hash值的高八位存储在bucket的tophash[i]中 用来快速判断key是否存在
  3. 当一个bucket满时 通过overflow指针连接到下一个bucket

bmap 结构体字段说明

type bmap struct{
    tophash [8]uint8
    keys [8]keytype 
    // keytype 由编译器编译时候确定
    values [8]elemtype 
    // elemtype 由编译器编译时候确定
    overflow uintptr 
    // overflow指向下一个bmap,overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}
  • tophash 用来快速查找key是否在该bucket中 一般会使用key的哈希值的高八位作为tophash值 存放在该字段中 当然它也会存储一些状态值来表示当前桶的状态
  • bmap中的key和value是各自放在一起的 存放形式如下 k1/k2/k3 ... ... v1/v2/v3 ... ... 这样子做的好处是会节省空间
  • 每个bucket中只能存放八个键值对 如果有了第九个 那么就会再创建一个bucket 使用指针链接起来

为什么说 k1/k2/k3 ... ... v1/v2/v3 ... ... 格式会比较省空间

比如说我们的key值有8个字节 value值只占1个字节

那么如果我们要按照 key / value 的方式来存储数据的话考虑到内存对齐 就要浪费额外的7个字节的空间

而我们上面的那种方式天然会内存对齐(一定是8的整数倍) 所以上面那种设计会更加巧妙一点

定位元素大体过程

定位元素过程如下

在这里插入图片描述

写保护监测

在我们开始定位时 我们首先会检查 map 的标志位 flags 如果此时 map 的写标志位被置1了则说明有其他线程在进行写操作

if h.flags&hashWriting != 0 {
    throw("concurrent map read and map write")
}
计算哈希值

之后我们就开始计算 key 的哈希值

hash := t.hasher(key, uintptr(h.hash0))               
//key经过哈希函数计算后,得到的哈希值如下(主流64位机下共 64 个 bit 位),不同类型的key会有不同的hash函数
10010111 | 00001111011011001000111100101010001001011001010101001010               
找到哈希对应的bucket
  • 定位规则 : 哈希值的低B个比特位 用来定位key所存放的bucket

B如何计算

B 是根据哈希表的当前桶的数量 N 来决定的 其目的是确定足够的比特位来唯一地表示哈希表中所有可能的桶索引 通常 B 是能够覆盖从 0 到 N-1 所有索引的最小比特数

如果 N 是 2 的幂 即 N = 2^B 那么 B 可以直接通过下面的公式计算出来:

B = log2(N)

假如说当前正在扩容中 并且还没有扩容完成 那么此时我们就使用旧的 bucket

hash := t.hasher(key, uintptr(h.hash0))
// 桶的个数m-1,即 1<<B-1,B=5时,则有0~31号桶
m := bucketMask(h.B)
// 计算哈希值对应的bucket
// t.bucketsize为一个bmap的大小,通过对哈希值和桶个数取模得到桶编号,通过对桶编号和buckets起始地址进行运算,获取哈希值对应的bucket
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 是否在扩容
if c := h.oldbuckets; c != nil {
  // 桶个数已经发生增长一倍,则旧bucket的桶个数为当前桶个数的一半
    if !h.sameSizeGrow() {
        // There used to be half as many buckets; mask down one more power of two.
        m >>= 1
    }
    // 计算哈希值对应的旧bucket
    oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
    // 如果旧bucket的数据没有完成迁移,则使用旧bucket查找
    if !evacuated(oldb) {
        b = oldb
    }
}
遍历bucket查找

我们通过哈希值的定位到了要查找的键值对是否在该bucket中之后

接着我们再通过 tophash 来快速在该bucket和该bucket的overflow中找出有没有对应的槽位

这里只会有两种可能

  • 找到了空槽位 即哈希表中没有对应的值 返回空指针
  • 找到了对应的槽位 即找到了对应的键值对 返回对应的指针

流程如下图

在这里插入图片描述

获取key 和 value对应位置

偏移量计算和定位公式如下

// keys的偏移量
dataOffset = unsafe.Offsetof(struct{
  b bmap
  v int64
}{}.v)

// 一个bucket的元素个数
bucketCnt = 8

// key 定位公式
k :=add(unsafe.Pointer(b),dataOffset+i*uintptr(t.keysize))

// value 定位公式
v:= add(unsafe.Pointer(b),dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
  • 第i个key值前面肯定有i个key 所以说位置偏移量要加上
  • 同理value值的前面要加上 所有的key便宜和i个value偏移

解决哈希冲突

Go语言也是通过一种特殊的开放定址法 它较为特殊的点在于后面链接的不是一个个元素 而是一个个bucket

负载因子

我们知道负载因子和哈希冲突的概率相关

在Go语言中

  • 1.1.7 即更早的版本 默认负载因子为6.5

这个负载因子主要是在空间浪费和减少哈希冲突之间做出了一个取舍 这是经过大量的实验得出的一个较为理想的数字

  • 1.1.8版本之后 map 的负载因子是根据实际的哈希表性能来动态调整

map扩容

在向 map 插入新 key 的时候 会进行条件检测 符合下面这 2 个条件 就会触发扩容

  • 超过负载 map 元素个数 > 6.5 * 桶个数
  • 溢出桶太多

溢出桶太多有两个判定标准

  • 当桶总数 < 2^15 时如果溢出桶总数 >= 桶总数 则认为溢出桶过多
  • 当桶总数 >= 2^15 时,直接与 2^15 比较 当溢出桶总数 >= 2^15 时 即认为溢出桶太多了

如果说是因为超出负载扩容 那么直接扩两倍

如果是因为溢出桶太多导致的 我们并不直接扩大容量 而是做一遍类似双倍扩容的动作 将松散的键值对排列紧密 节省空间 提高bucket的使用率

需要注意的点

Go map遍历是无序的

使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同

这是 Go 语言的设计者们有意为之 旨在提示开发者们 Go 底层实现并不保证 map 遍历顺序稳定 不要依赖 range 遍历结果顺序

主要原因有2点:

  • map在遍历时 并不是从固定的0号bucket开始遍历的 每次遍历 都会从一个随机值序号的bucket 再从其中随机的cell开始遍历
  • map在扩容后 会发生key值迁移

如果设计者不增加随机数 那么一些熟悉map特性的开发者可能会自以为是的认为map是相对有序的 从而导致一些错误的发生

map是不支持并发读写的

map是不支持并发读写的 如果我们并发的去读写则会产生panic

如何线程安全的使用map

map本身不是线程安全的 如果我们想要其线程安全则要进行同步

  • 我们可以使用 sync.Mutexsync.RWMutex进行加锁
  • 使用go官方提供的sync.Map替代map

map中的key可以取地址吗

不可以 我们上面也说过了 key的地址是会不断发生变化的 所以说下面的这段代码是错误的

type Student struct {
     name string
} 
func main() { 
    m := map[string]Student{"people": {"zhoujielun"}} 
    m["people"].name = "wuyanzu"
}

如果想要修改 我们通常可以这样做

func main() {
    m := map[string]Student{"people": {"zhoujielun"}}
    // 创建一个副本,修改副本
    student := m["people"]
    student.name = "wuyanzu"
    // 将修改后的副本放回 map 中
    m["people"] = student
}

又或者 我们直接将value变成一个地址 这样子就可以直接修改啦

func main() {
    m := map[string]*Student{"people": {"zhoujielun"}}
    // 由于 map 中存储的是指向 Student 的指针,可以直接修改
    m["people"].name = "wuyanzu"
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/213525.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QueryRunner报红处理

如图&#xff0c;有同学反映QueryRunner报红&#xff0c;就是没有导包 自己去找项目的地址&#xff0c;找到web文件夹下的WEB-INF 把这些jar包都粘贴进去&#xff0c;以后项目基本都会用到的&#xff0c;资源自己去找 粘贴好后打开文件的Project Structure 点击Dependencies 点…

github打不开,全网最简单解决方法,没有之一

下载watt toolkit&#xff0c; 选择‘github’&#xff0c;点击‘一键加速’&#xff0c; 具体步骤如下&#xff1a;去电脑微软商店下载watt toolkit&#xff0c;或者直接打开网址https://apps.microsoft.com/detail/9MTCFHS560NG?hlen-us&glUS 如图&#xff0c;点击安装i…

洛谷 B2006 地球人口承载力估计 C++代码

目录 前言 思路点拨 AC代码 结尾 前言 今天我们来做洛谷上的一道题目。 网址&#xff1a;地球人口承载力估计 - 洛谷 题目&#xff1a; 思路点拨 经典牛吃草问题。 解设一个人一年吃一份草。 则x*a-y*b为会多出的草&#xff0c;为什么会多呢&#xff1f;是因为每年都有…

Vue3-路由

VueRouter4路由语法解析 1.创建路由实例由createRouter实现 2.路由模式 1&#xff09;history模式使用createWebHistory()&#xff1a;地址栏不带# 2&#xff09;hash模式使用createWebHashHistory()&#xff1a;地址栏带# 3&#xff09;参数是基础路径&#xff0c;默认/ …

智跃人力资源管理系统GenerateEntityFromTable.aspx接口存在SQL注入漏洞 附POC

@[toc] 智跃人力资源管理系统GenerateEntityFromTable.aspx接口存在SQL注入漏洞 附POC 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者…

弦理论的技术探索

弦理论的技术探索 一、引言 弦理论,作为现代物理学中的一个重要分支,旨在揭示宇宙的终极规律。它认为,宇宙中的一切物质和能量都是由微小的弦振动产生的。本文将深入探讨弦理论的技术层面,包括其数学基础、物理应用以及计算机模拟等方面。 二、弦理论的数学基础 弦理论的…

【Delphi】中使用Indy进行UDP广播通信

目录 一、服务器端&#xff08;接收端&#xff09; 二、客户端&#xff08;广播端&#xff09; Delphi中进行UDP广播通信函数代码&#xff1a; 一、服务器端&#xff08;接收端&#xff09; 在主界面上返放置一个TIdUDPServer控件&#xff0c;设置好该控件的监听端口&#…

C++笔试训练day_1

文章目录 选择题编程题 选择题 编程题 #include <iostream> #include <algorithm> #include <vector>using namespace std;int main() {int n 0;cin >> n;vector<int> v;v.resize(3 * n);int x 0;for(int i 0; i < v.size(); i){cin >&…

94基于matlab的蚁群算法 (ACO) 对付的图像边缘检测问题

基于matlab的蚁群算法 (ACO) 对付的图像边缘检测问题。提出基于蚁群算法的边缘检测方法是能够建立一个信息素矩阵表示提出了一种在图像每个像素位置的边缘信息根据大量的蚂蚁的运动有哪些派去在图像上移动。此外&#xff0c;运动这些蚂蚁是由图像的局部变化驱动强度值。数据可更…

什么是Anaconda

Anaconda的安装也很方便。打开这个网站Anaconda下载&#xff0c;然后安装即可。 Anaconda可以帮助我们解决团队之间合作的包依赖管理问题。在没有使用Anaconda之前&#xff0c;如果你的Python程序想让你的同事运行&#xff0c;那么你的同事可能会遇到很多包依赖问题&#xff0…

调优--学习笔记

1&#xff0c;Presto调优 数据存储格式 1&#xff09;合理设置分区 与Hive类似&#xff0c;Presto会根据元信息读取分区数据&#xff0c;合理的分区能减少Presto数据读取量&#xff0c;提升查询性能。 2&#xff09;使用列式存储 Presto对ORC文件读取做了特定优化&#xff0c…

【Python】tensorflow学习的个人纪录(2)

actor.learn(s, a, td_error)def learn(self, s, a, td):s s[np.newaxis, :]feed_dict {self.s: s, self.a: a, self.td_error: td}_, exp_v self.sess.run([self.train_op, self.exp_v], feed_dict)return exp_v输入变量的数值&#xff1a; 步进&#xff1a; []---->[…

ER图是什么,怎么画?

ER图&#xff08;Entity-Relationship Diagram&#xff09;是一种用于描述实体间关系的图形化表示方法。它主要用于数据库设计&#xff0c;可以清晰地展示实体、属性和实体间的联系。常用的ER图类型包括&#xff1a; 实体-关系模型&#xff08;Entity-Relationship Model&…

最新最全的Postman接口测试: postman实现参数化

什么时候会用到参数化 比如&#xff1a;一个模块要用多组不同数据进行测试 验证业务的正确性 Login模块&#xff1a;正确的用户名&#xff0c;密码 成功&#xff1b;错误的用户名&#xff0c;正确的密码 失败 postman实现参数化 在实际的接口测试中&#xff0c;部分参数…

WordPress定时文章自动发布技巧

对于许多WordPress站长来说&#xff0c;文章的管理和发布计划往往是一个头疼的问题。随着内容的不断增加&#xff0c;时间表的调整以及发布频率的把握成为了让人焦头烂额的挑战。 一、时间管理难题 对于博客管理员来说&#xff0c;时间管理一直是个令人困扰的问题。在忙碌的生…

Vue3实现滚动到容器底部时发送请求,加载新数据

问题来源 在项目中出现了需要在容器滚动到底部时&#xff0c;加载新的数据的需求&#xff0c;以下是解决的方案笔记 解决 画了个流程图&#xff1a; 如图&#xff0c;先添加一个动态加载的图标&#xff0c;还有全部数据载完的《到底啦...》 大概这么个样子&#xff0c;之后呢…

苍穹外卖——地址簿功能

地址簿功能代码 1. 地址簿功能 1.1 需求分析和设计 查询地址列表新增地址修改地址删除地址设置默认地址查询默认地址 1.1.1 接口设计 根据上述原型图先粗粒度设计接口&#xff0c;共包含7个接口。 接口设计&#xff1a; 新增地址查询登录用户所有地址查询默认地址根据id…

C++基础 -35- string类

string类的格式 string a;如下图&#xff0c;使用string类比常规的字符串处理方便很多 而且需要进行的字符串处理&#xff0c;在类中都能完成 #include "iostream"using namespace std;extern "C" {#include "string.h" }int main() {//c的写…

SpringBoot框架结合Redis实现分布式锁

一、SpringBoot结合 Redis实现分布式锁 1.1、什么是分布式锁 分布式锁&#xff0c;是在分布式的环境下&#xff0c;才会使用到的一种同步访问机制&#xff0c;在传统的单体环境里面&#xff0c;不存在分布式锁的概念&#xff0c;只有在分布式环境里面&#xff0c;才有分布式锁…

【Python】tensorflow学习的个人纪录(3)

sess tf.Session()actor Actor(sess, n_featuresN_S, lrLR_A, action_bound[-A_BOUND, A_BOUND])步进&#xff1a;