Go语言Map的底层原理

概念

map 又称字典,是一种常用的数据结构,核心特征包含下述三点:

(1)存储基于 key-value 对映射的模式;

(2)基于 key 维度实现存储数据的去重;

(3)读、写、删操作控制,时间复杂度 O(1).

****key 的类型要求
map 中,key 的数据类型必须为可比较的类型,切片、map、func不可比较

指针类型是可以比较的。

如果是结构体会怎么样?

结构体中的所有字段的类型都必须是可比较的类型的才能作为map的key

type student struct {name stringage  int
}func TestMap(t *testing.T) {var m map[student]stringm = map[student]string{student{"Jane", 20}: "Jane",}t.Log(m)
}

同理数组也是一样。

遍历

在执行 map 遍历操作时,获取的 key-value 对并没有一个固定的顺序,因此前后两次遍历顺序可能存在差异.

并发冲突

map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.

具体规则是:

(1)并发读没有问题;

(2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;

(3)读的时候发现其他 goroutine 在并发写,抛出 fatal error;

(4)写的时候发现其他 goroutine 在并发写,抛出 fatal error.

fatal("concurrent map read and map write")
fatal("concurrent map writes")

需要关注,此处并发读写会引发 fatal error,是一种比 panic 更严重的错误,无法使用 recover 操作捕获.

核心原理

map 又称为 hash map,在算法上基于 hash 实现 key 的映射和寻址;在数据结构上基于桶数组实现 key-value 对的存储.

以一组 key-value 对写入 map 的流程为例进行简述:

(1)通过哈希方法取得 key 的 hash 值;

(2)hash 值对桶数组长度取模,确定其所属的桶;

(3)在桶中插入 key-value 对.

hash 的性质,保证了相同的 key 必然产生相同的 hash 值,因此能映射到相同的桶中,通过桶内遍历的方式锁定对应的 key-value 对.

因此,只要在宏观流程上,控制每个桶中 key-value 对的数量,就能保证 map 的几项操作都限制为常数级别的时间复杂度.

Hash

hash 译作散列,是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间(这里的压缩是将无限的输入空间压缩成有限的输出域),因此不同输入可能会映射成相同的输出结果. 此外,hash在压缩过程中会存在部分信息的遗失,因此这种映射关系具有不可逆的特质.

(1)hash 的可重入性:相同的 key,必然产生相同的 hash 值;

(2)hash 的离散性:只要两个 key 不相同,不论其相似度的高低,产生的 hash 值会在整个输出域内均匀地离散化;

(3)hash 的单向性:企图通过 hash 值反向映射回 key 是无迹可寻的.

(4)hash 冲突:由于输入域(key)无穷大,输出域(hash 值)有限,因此必然存在不同 key 映射到相同 hash 值的情况,称之为 hash 冲突.

可能会感到有点冲突,但(2)和(4)并不相矛盾,离散型好的哈希函数只是减少冲突的概率,并不能完全避免。

桶数组

map中,会通过长度为2的整数次幂的桶数组进行 key-value 对的存储:

(1)每个桶固定可以存放8个key-value对

(2)倘若超过 8 个 key-value 对打到桶数组的同一个索引当中,此时会通过创建桶链表的方式来化解这一问题。

在这里插入图片描述

1.:hash冲突不同的key,可能会存在相同的hash值

2:不同的hash值通过对桶长度进行取模之后,也有可能会被打到同一个桶中。

综上面两点:不同的 key-value 可能被映射到 map 的同一个桶当中。

拉链法解决hash冲突

拉链法中,将命中同一个桶的元素通过链表的形式进行链接,因此很便于动态扩展.

Go map 的做法(拉链法的优化):

  • 每个 bucket 固定容纳 8 个 key-value
  • 如果满了,就通过 overflow 字段挂一个溢出桶
  • 实际上是一个 结构化拉链法(struct-based chaining)

开放寻址法解决hash冲突

所有元素都存在 哈希表本身。如果发生冲突,就按照某种“探测序列”寻找下一个可用位置。

常见策略:

  • 线性探测(index+1
  • 二次探测(index+1^2, +2^2…)
  • 双重哈希(用另一个哈希函数重新计算偏移)
方法优点
拉链法简单常用;无需预先为元素分配内存。
开放寻址法无需额外的指针用于链接元素;内存地址完全连续,可以基于局部性原理,充分利用CPU高速缓存。

Go 的 map 实现确实结合了 开放寻址法拉链法 的思想,但它并不是标准意义上的链表拉链法,而是桶链表 + 定长数组 + 溢出桶机制的混合方案。

(1) 每个桶是一个结构体(Go 源码中为bmap),包含:

一个 tophash 数组(快速判断 key 匹配),数组长度为8,也就是说一个桶中可以存储8个Key-Value.

当桶满了,不是在桶内继续链表扩展 key-value,而是通过一个 溢出指针数组

指向额外的桶(overflow bucket)

所以Go的map实际上是:哈希桶数组+每桶最多存储8个kv+桶装满后追加溢出桶形成“桶链表”

// A bucket for a Go map.
type bmap struct {// tophash generally contains the top byte of the hash value// for each key in this bucket. If tophash[0] < minTopHash,// tophash[0] is a bucket evacuation state instead.tophash [abi.OldMapBucketCount]uint8// Followed by bucketCnt keys and then bucketCnt elems.// NOTE: packing all the keys together and then all the elems together makes the// code a bit more complicated than alternating key/elem/key/elem/... but it allows// us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}

(2)key 命中一个桶后,在桶的 8 个位置中寻找空位插入,这就是类似开放寻址法的本地桶内查找

(3)如果桶的 8 个位置都被占满,则找下一个溢出桶,重复第(2)步

(4)如果遍历所有溢出桶都没有空位,则新建溢出桶并插入

Go map 扩容机制

为什么要扩容?

如果桶数组的长度一直保持不变,那么随着key-value对的增长,当一个桶下挂载的key-value达到一定的量级,时间复杂度上升,无法满足诉求。

因此,为了将操作的时间复杂度维持在 O(1),map 会在满足一定条件时触发扩容,以控制每个桶的平均负载在常量级别。

map 扩容机制的核心点包括:

(1)扩容分为增量扩容和等量扩容;

(2)增量扩容:当 map 的负载因子(key 数量 / 桶数量)大于 6.5 时,会触发增量扩容,将桶数组长度扩大为原值的两倍。

(3) 等量扩容:当桶内溢出桶数量大于等于 2^B 时( B 为桶数组长度的指数,B 最大取 15),发生等量扩容,桶的长度保持为原值;等量扩容用于处理 hash 分布不均(热点 key)导致的溢出桶爆炸问题,此时不会改变桶数量,而是重新散列所有 key。

(4)采用渐进扩容的方式,当桶被实际操作到时,由使用者负责完成数据迁移,避免因为一次性的全量数据迁移引发性能抖动。渐进迁移通过“读写操作触发搬迁”的方式,把迁移成本分摊到用户的操作中,避免瞬时卡顿。

渐进扩容的核心流程

扩容时,Go map 并不会马上把所有旧桶迁移完,而是:

  1. 分配一份新的 bucket 数组(oldbuckets 和 newbuckets 并存);
  2. 设置 oldbuckets 指针指向旧桶;
  3. 在后续的 每次写入操作(插入/删除)中顺便“搬一两个旧桶”的数据到新桶中
  4. 迁移的数据会被重新计算 hash 值并分配到新桶;
  5. 同时维护一个 nevacuate 指针记录迁移进度,当 nevacuate 指向了所有旧桶的末尾,说明所有桶都搬迁完毕。
  6. 当所有旧桶都迁移完毕后,map 会把旧桶释放,指针 oldbuckets 清空。才切换为新桶数组。

在 map 渐进扩容过程中,如何判断一个旧桶的 key 应该迁移到新桶的哪个桶中?

在 Go 的 map 中,扩容时新桶数量 = 旧桶数量 × 2(增量扩容)

因此:一个旧桶中的 key-value 对只可能被迁移到两个新桶中的一个。

判断规则是:对于旧桶中每个 key,根据 hash 值重新计算位置,看是落到原桶 b,还是 b + oldBucketCount

因为旧桶的索引是 h & (oldBucketCount - 1),即取 hash 的低 B-1 位。

扩容后只是多了一位(第 B 位),也就是说:

  • 如果这第 B 位是 0:落到原桶 b;
  • 如果这第 B 位是 1:落到新桶 b + oldBucketCount。

Go map的桶(bucket)结构是:

  • 每个桶可以存放 最多 8 个 key-value 对
  • 为了加快桶内查找效率,Go 把 每个 key 的 hash 值的“高 8 位” 存进一个 tophash 数组(长度为 8,对应每个 key);
  • 而 hash 值的 低若干位(具体取决于桶数量) 用于决定 key 属于哪个桶。

数据结构

hmap

type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // # live cells == size of map.  Must be first (used by len() builtin)flags     uint8B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0     uint32 // hash seedbuckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growingnevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)clearSeq   uint64extra *mapextra // optional fields
}

(1)count:map 中的 key-value 总数;

(2)flags:map 状态标识,可以标识出 map 是否被 goroutine 并发读写;

(3)B:桶数组长度的指数,桶数组长度为 2^B;

(4)noverflow:map 中溢出桶的数量;

(5)hash0:hash 随机因子,生成 key 的 hash 值时会使用到;

(6)buckets:桶数组;

(7)oldbuckets:扩容过程中老的桶数组;

(8)nevacuate:扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中;

(9)extra:预申请的溢出桶.

mapextra

这个mapextra是hmap中的一个“辅助字段”,专门用于管理溢出桶(overflow buckets)

type mapextra struct {// If both key and elem do not contain pointers and are inline, then we mark bucket// type as containing no pointers. This avoids scanning such maps.// However, bmap.overflow is a pointer. In order to keep overflow buckets// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.// overflow and oldoverflow are only used if key and elem do not contain pointers.// overflow contains overflow buckets for hmap.buckets.// oldoverflow contains overflow buckets for hmap.oldbuckets.// The indirection allows to store a pointer to the slice in hiter.overflow    *[]*bmapoldoverflow *[]*bmap// nextOverflow holds a pointer to a free overflow bucket.nextOverflow *bmap
}

在 map 初始化时,倘若容量过大,会提前申请好一批溢出桶,以供后续使用,这部分溢出桶存放在 hmap.mapextra 当中:

(1)mapextra.overflow:供桶数组 buckets 使用的溢出桶;

(2)mapextra.oldoverFlow: 扩容流程中,供老桶数组 oldBuckets 使用的溢出桶;

(3)mapextra.nextOverflow:下一个可用的溢出桶.

Go 会需要一个“溢出桶”来存放新插入的数据,这时候它有一个“拿溢出桶”的优先顺序

  1. 优先从 mapextra.nextOverflow:这个是预先分配好的一批溢出桶
  2. 如果 nextOverflow 没桶了,就:去 overflow[] 列表里找是否有空闲的桶(可能是之前用过又回收的)
  3. 如果都没有,才会向 Go 的内存分配器(heap)重新申请一个新的溢出桶

bmap

// A bucket for a Go map.
type bmap struct {// tophash generally contains the top byte of the hash value// for each key in this bucket. If tophash[0] < minTopHash,// tophash[0] is a bucket evacuation state instead.tophash [abi.OldMapBucketCount]uint8// Followed by bucketCnt keys and then bucketCnt elems.// NOTE: packing all the keys together and then all the elems together makes the// code a bit more complicated than alternating key/elem/key/elem/... but it allows// us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}

(1)bmap 就是 map 中的桶,可以存储 8 组 key-value 对的数据,以及一个指向下一个溢出桶的指针;

(2)每组 key-value 对数据包含 key 高 8 位 hash 值 tophash,key 和 val 三部分;

(3)在代码层面只展示了 tophash 部分,但由于 tophash、key 和 val 的数据长度固定,因此可以通过内存地址偏移的方式寻找到后续的 key 数组、val 数组以及溢出桶指针;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

循环神经网络(RNN):原理、架构与实战

循环神经网络&#xff08;Recurrent Neural Network, RNN&#xff09;是一类专门处理序列数据的神经网络&#xff0c;如时间序列、自然语言、音频等。与前馈神经网络不同&#xff0c;RNN 引入了循环结构&#xff0c;能够捕捉序列中的时序信息&#xff0c;使模型在不同时间步之间…

java 项目登录请求业务解耦模块全面

登录是统一的闸机&#xff1b; 密码存在数据库中&#xff0c;用的是密文&#xff0c;后端加密&#xff0c;和数据库中做对比 1、UserController public class UserController{Autowiredprivate IuserService userservicepublic JsonResult login(Validated RequestBody UserLo…

【手写数据库核心揭秘系列】第9节 可重入的SQL解析器,不断解析Structure Query Language,语言翻译好帮手

可重入的SQL解析器 文章目录 可重入的SQL解析器一、概述 二、可重入解析器 2.1 可重入设置 2.2 记录状态的数据结构 2.3 节点数据类型定义 2.4 头文件引用 三、调整后的程序结构 四、总结 一、概述 现在就来修改之前sqlscanner.l和sqlgram.y程序,可以不断输入SQL语句,循环执…

微软开源bitnet b1.58大模型,应用效果测评(问答、知识、数学、逻辑、分析)

微软开源bitnet b1.58大模型,应用效果测评(问答、知识、数学、逻辑、分析) 目 录 1. 前言... 2 2. 应用部署... 2 3. 应用效果... 3 1.1 问答方面... 3 1.2 知识方面... 4 1.3 数字运算... 6 1.4 逻辑方面... …

用HTML5+JavaScript实现汉字转拼音工具

用HTML5JavaScript实现汉字转拼音工具 前一篇博文&#xff08;https://blog.csdn.net/cnds123/article/details/148067680&#xff09;提到&#xff0c;当需要将拼音添加到汉字上面时&#xff0c;用python实现比HTML5JavaScript实现繁琐。在这篇博文中用HTML5JavaScript实现汉…

鸿蒙OSUniApp 开发的动态背景动画组件#三方框架 #Uniapp

使用 UniApp 开发的动态背景动画组件 前言 在移动应用开发中&#xff0c;动态背景动画不仅能提升界面美感&#xff0c;还能增强用户的沉浸感和品牌辨识度。无论是登录页、首页还是活动页&#xff0c;恰到好处的动态背景都能让产品脱颖而出。随着鸿蒙&#xff08;HarmonyOS&am…

云原生技术架构技术探索

文章目录 前言一、什么是云原生技术架构二、云原生技术架构的优势三、云原生技术架构的应用场景结语 前言 在当今的技术领域&#xff0c;云原生技术架构正以一种势不可挡的姿态席卷而来&#xff0c;成为了众多开发者、企业和技术爱好者关注的焦点。那么&#xff0c;究竟什么是…

AWS之AI服务

目录 一、AWS AI布局 ​​1. 底层基础设施与芯片​​ ​​2. AI训练框架与平台​​ ​​3. 大模型与应用层​​ ​​4. 超级计算与网络​​ ​​与竞品对比​​ AI服务 ​​1. 机器学习平台​​ ​​2. 预训练AI服务​​ ​​3. 边缘与物联网AI​​ ​​4. 数据与AI…

lwip_bind、lwip_listen 是阻塞函数吗

在 lwIP 协议栈中&#xff0c;lwip_bind 和 lwip_listen 函数本质上是非阻塞的。 通常&#xff0c;bind和listen在大多数实现中都是非阻塞的&#xff0c;因为它们只是设置套接字的属性&#xff0c;不需要等待外部事件。阻塞通常发生在接受连接&#xff08;accept&#xff09;、…

【后端高阶面经:消息队列篇】28、从零设计高可用消息队列

一、消息队列架构设计的核心目标与挑战 设计高性能、高可靠的消息队列需平衡功能性与非功能性需求,解决分布式系统中的典型问题。 1.1 核心设计目标 吞吐量:支持百万级消息/秒处理,通过分区并行化实现横向扩展。延迟:端到端延迟控制在毫秒级,适用于实时业务场景。可靠性…

【运维实战】Linux 内存调优之进程内存深度监控

写在前面 内容涉及 Linux 进程内存监控 监控方式包括传统工具 ps/top/pmap ,以及 cgroup 内存子系统&#xff0c;proc 内存伪文件系统 监控内容包括进程内存使用情况&#xff0c; 内存全局数据统计&#xff0c;内存事件指标&#xff0c;以及进程内存段数据监控 监控进程的内…

决策树 GBDT XGBoost LightGBM

一、决策树 1. 决策树有一个很强的假设&#xff1a; 信息是可分的&#xff0c;否则无法进行特征分支 2. 决策树的种类&#xff1a; 2. ID3决策树&#xff1a; ID3决策树的数划分标准是信息增益&#xff1a; 信息增益衡量的是通过某个特征进行数据划分前后熵的变化量。但是&…

java基础学习(十四)

文章目录 4-1 面向过程与面向对象4-2 Java语言的基本元素&#xff1a;类和对象面向对象的思想概述 4-3 对象的创建和使用内存解析匿名对象 4-1 面向过程与面向对象 面向过程(POP) 与 面向对象(OOP) 二者都是一种思想&#xff0c;面向对象是相对于面向过程而言的。面向过程&…

TCP 三次握手,第三次握手报文丢失会发生什么?

文章目录 RTO(Retransmission Timeout)注意 客户端收到服务端的 SYNACK 报文后&#xff0c;会回给服务端一个 ACK 报文&#xff0c;之后处于 ESTABLISHED 状态 因为第三次握手的 ACK 是对第二次握手中 SYN 的确认报文&#xff0c;如果第三次握手报文丢失了&#xff0c;服务端就…

deepseek告诉您http与https有何区别?

有用户经常问什么是Http , 什么是Https &#xff1f; 两者有什么区别&#xff0c;下面为大家介绍一下两者的区别 一、什么是HTTP HTTP是一种无状态的应用层协议&#xff0c;用于在客户端浏览器和服务器之间传输网页信息&#xff0c;默认使用80端口 二、HTTP协议的特点 HTTP协议…

openresty如何禁止海外ip访问

前几天&#xff0c;我有一个徒弟问我&#xff0c;如何禁止海外ip访问他的网站系统&#xff1f;操作系统采用的是centos7.9&#xff0c;发布服务采用的是openresty。通过日志他发现&#xff0c;有很多类似以下数据 {"host":"172.30.7.95","clientip&q…

理解 Redis 事务-20 (MULTI、EXEC、DISCARD)

理解 Redis 事务&#xff1a;MULTI、EXEC、DISCARD Redis 事务允许你将一组命令作为一个单一的原子操作来执行。这意味着事务中的所有命令要么全部执行&#xff0c;要么全部不执行。这对于在需要一起执行多个操作时保持数据完整性至关重要。本课程将涵盖 Redis 事务的基础知识…

Milvus分区-分片-段结构详解与最佳实践

导读&#xff1a;在构建大规模向量数据库应用时&#xff0c;数据组织架构的设计往往决定了系统的性能上限。Milvus作为主流向量数据库&#xff0c;其独特的三层架构设计——分区、分片、段&#xff0c;为海量向量数据的高效存储和检索提供了坚实基础。 本文通过图书馆管理系统的…

Kettle 远程mysql 表导入到 hadoop hive

kettle 远程mysql 表导入到 hadoop hive &#xff08;教学用 &#xff09; 文章目录 kettle 远程mysql 表导入到 hadoop hive创建 对象 执行 SQL 语句 -mysql 导出 CSV格式CSV 文件远程上传到 HDFS运行 SSH 命令远程登录 run SSH 并执行 hadoop fs -put 建表和加载数据总结 创…

Linux输出命令——echo解析

摘要 全面解析Linux echo命令核心功能&#xff0c;涵盖文本输出、变量解析、格式控制及高级技巧&#xff0c;助力提升Shell脚本开发与终端操作效率。 一、核心功能与定位 作为Shell脚本开发的基础工具&#xff0c;echo命令承担着信息输出与数据传递的重要角色。其主要功能包…