Go Map 源码分析(一)

Go语言中的map是通过哈希表实现的,其底层结构和实现机制如下:

一、hash 结构

hmap结构体:是map的头部结构,主要字段及含义如下:

  • count:表示当前哈希表中的元素数量,与len()函数相对应。
  • flags:标记字段,用于标记是否正在进行读写操作,以便实现并发读写的检测。 所以它不是并发安全的
  • B:表示当前哈希表持有的buckets数量的对数,即len(buckets) == 2^B。
  • noverflow:溢出桶的大致数量。
  • hash0:hash种子。
  • buckets:存储2^B个桶的数组,是一个unsafe.Pointer,因为Go语言中支持不同类型的键值对,需要在编译时才能确定map的类型。
  • oldbuckets:扩容时用于保存之前的buckets的字段,大小是buckets的一半。
  • nevacuate:迁移进度计数器,记录buckets中小于该值的bucket已经完成迁移。
  • extra:指向mapextra结构体的指针,用于存储一些可选字段。
type hmap struct {// 元素个数,调用 len(map) 时,直接返回此值 count intflags uint8// buckets 的对数 log_2B uint8// overflow 的 bucket 近似数noverflow uint16// 计算 key 的哈希的时候会传入哈希函数hash0 uint32// 指向 buckets 数组,大小为 2^B// 如果元素个数为 0,就为 nilbuckets unsafe.Pointer// 扩容的时候,buckets 长度会是 oldbuckets 的两倍 oldbuckets unsafe.Pointer// 指示扩容进度,小于此地址的 buckets 完成迁移 nevacuate uintptrextra *mapextra
}

bmap结构体:是哈希表中的桶,每个bmap能够存储8个键值对,并且设有一个指针,当某个bmap存满时,就会申请新的bmap进行存储,并与前一个bmap构成链表。其结构如下:

  • tophash:数组,用于存储每个key hash之后的高位hash值。
  • keys:数组,用于存储key。
  • elems:数组,用于存储value。
  • overflow:溢出指针,指向下一个bmap的地址。

下面是map 的初始形态,

type bmap struct {tophash [bucketCnt]uint8
}

但是编译器会对go 的map 给塞几个字段

type bmap struct { topbits [8]uint8keys [8]keytypevalues  [8]valuetypepad uintptroverflow uintptr
}

当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 GC 时扫描整个 hmap,提升效率。

但是map 中是包含了一个 overflow 字段的, 这个字段是指针类型的, 这个时候我们可能会把相应的值移动到extra *mapextra 这个字段上来, 并且会开启maxarea的两个字段启用 overflow 和 oldoverflow 字段。

mapextra结构体:主要字段如下:

  • overflow:指向当前buckets的溢出桶数组的指针。
  • oldoverflow:指向oldbuckets的溢出桶数组的指针。
  • nextOverflow:指向还未使用的、提前分配的溢出桶链表。

二、哈希

map 的一个关键点在于哈希函数的选择。在程序启动时,Go 会检测 CPU 是否支持 aes,如果支持, 则使用 aes hash,否则使用 memhash。这在函数 alginit()中完成,源码位于路径 src/runtime/alg.go 下。对于 hash 函数,有加密型和非加密型。加密型的一般用于加密数据、数字摘要等,典型代表 就是 md5、sha1、sha256、aes256 这类;非加密型的一般就是查找,如 MurmurHash 等

Go语言中map的哈希函数会根据键的类型和值来计算一个哈希值,这个哈希值是一个32位或64位的整数。哈希函数的设计目标是尽量使不同的键映射到不同的哈希值,以减少哈希冲突。对于不同的键类型,Go语言会采用不同的哈希算法,例如对于字符串键,会根据字符串的内容计算哈希值;对于整数键,则直接使用整数本身或其某种变换作为哈希值。

但计算它到底要落在哪个 bucket 时,只会用到最后 B 个 bit 位。如果 B = 5,那么桶的数量,也就是 buckets 数组的长度是 2^5 = 32。例如,现在有一个 key 经过哈希函数计算后,得到的哈希结果是:

10010111 | 000011110110110010001111001010100010010110010101010 | 00110
  1. 首先会根据后面的5 位去定位到对应的桶的位置, 这里表示第6号桶
  2. 然后取hash 值的高8 位, 找到key 在桶里面的位置

因为根据后 B 个 bit 位决定 key 落入的 bucket 编号,也就是桶编号,因此肯定会存在冲突。当两个不同的 key 落在同一个桶中,也就是发生了哈希冲突。冲突的解决手段是用链表法:在 bucket 中,从前往后找到第一个空位,放入新加入的有冲突的 key。之后,在查找某个 key 时,
先找到对应的桶,再去遍历 bucket 中所有的 key

key, value的内存布局其实也是比较有意思的, 它的<key, value> 不是放在一起的:

在这里插入图片描述

三、Map 赋值

如果是map 的 assign 的过程情况, 可能又会存在不同, 因为赋值操作可能存在两种情况

  • 插入操作: 当前的 key 不存在, 我们需要插入
  • 修改操作, 当前的 key 存在, 我们直接修改即可

map 的赋值操作:

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer

整体而言,map的赋值流程十分简洁。首先对key计算hash值,依据该值按照既定流程确定赋值位置,可能是插入新key,也可能是更新旧key,然后在相应位置执行赋值操作。核心在于一个双层循环:外层循环遍历bucket及overflow bucket,内层循环遍历单个bucket的所有槽位。下面我们分别来看:

mapassign函数会先检查map的标志位flags,若flags的写标志位被置为1,意味着有其他协程正在进行写操作,而assign同样是写操作,这将导致并发写冲突,从而使程序直接panic,这也表明map不具备协程安全性。

//当存在并发竞争的时候
if h.flags&hashWriting != 0 {fatal("concurrent map writes")
}
// 否则设置为正在读取
h.flags ^= hashWriting

然后我们检查当前的map 时候需要进行扩容,当 map 处于扩容阶段,定位 key 到某个 bucket 后,需确保该 bucket 对应的老 bucket 已完成迁移,即老 bucket 中的 key 都已迁移到新 bucket(老bucket中的key会被分散到两个新bucket),之后才能在新bucket中进行插入或更新操作。只有完成迁移,才能安全地在新bucket里确定key的安置地址,进而进行后续赋值操作。

// 定位到对应的bucket
bucket := hash & bucketMask(h.B)// 判断当前的 hmap 是否在增长中
if h.growing() {growWork(t, h, bucket)
}

扩容完成就是核心的两层循环:

// 首先是一层循环:
for {// 然后是遍历所有的mapfor i := uintptr(0); i < abi.MapBucketCounter; i++ {}
}
1. 未匹配

在循环的过程中, 首先判断一下当前的 tophash[i] 是不是和 key 的hash 相等, 如果不相等继续判断inserti 是不是为null, 如果为null, 说明当前这个位置是一个空的位置,

if b.tophash[i] != top {if isEmpty(b.tophash[i]) && inserti == nil {}
}

准备两个指针,inserti 指向 key 的 hash 值在 tophash 数组的位置,insertk 指向 cell 的位置,即 key 最终放置的地址。而对应value的位置则容易计算,tophash数组中的索引位置决定了key在整个bucket中的位置(共8个key),value的位置需跨过8个key的长度。

inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))

如果在之后的过程中依然没有在map 中发现匹配的key, 就会跳出循环, 在inserti, insertk, elem 这个位置插入对应的值

2. 匹配

如果我们在遍历的过程中匹配了对应的top hash,

首先会使用 t.IndirectKey() 检查是否需要间接访问键。如果键的大小超过指针大小(通常是 8 字节),Go 的 map 实现会使用间接存储(即存储指针而不是直接存储键值)如果需要间接访问,k 被更新为指向实际键的指针。

if t.IndirectKey() {k = *((*unsafe.Pointer)(k))
}

下面就是更新map中的键值对的更新操作:

if t.NeedKeyUpdate() {typedmemmove(t.Key, k, key)
}

跳出循环后如果满足扩容条件,则会主动触发一次扩容操作, 扩容的时候还需要重新走一遍上面的过程, 这是因为扩容之后 key 分布出现在新的位置。

// 判断当前map 存储的元素是否过多
if !h.growing() && (overLoadFactor(h.count + 1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {hashGrow(t, h)goto again
}

如果不满足对应的扩容条件,我们会判断 inserti 是否为 nil, 如果为nil, 说明我们需要创建一个overflow, 更新对应的值:

newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, abi.MapBucketCount*uintptr(t.KeySize))

最后,会更新 map 相关的值,如果是插入新 key,map 的元素数量字段 count 值会加 1;并 且会将 hashWriting 写标志位清零。

typedmemmove(t.Key, insertk, key)
*inserti = top
h.count++

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

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

相关文章

Linux-C/C++--深入探究文件 I/O (上)(文件的管理、函数返回错误、exit()、_Exit()、_exit())

经过上一章内容的学习&#xff0c;相信各位读者对 Linux 系统应用编程中的基础文件 I/O 操作有了一定的认识和理解了&#xff0c;能够独立完成一些简单地文件 I/O 编程问题&#xff0c;如果你的工作中仅仅只是涉及到一些简单文件读写操作相关的问题&#xff0c;其实上一章的知识…

【机器学习实战中阶】音乐流派分类-自动化分类不同音乐风格

音乐流派分类 – 自动化分类不同音乐风格 在本教程中,我们将开发一个深度学习项目,用于自动化地从音频文件中分类不同的音乐流派。我们将使用音频文件的频率域和时间域低级特征来分类这些音频文件。 对于这个项目,我们需要一个具有相似大小和相似频率范围的音频曲目数据集…

Walrus Learn to Earn计划正式启动!探索去中心化存储的无限可能

本期 Learn to Earn 活动将带领开发者和区块链爱好者深入探索 Walrus 的技术核心与实际应用&#xff0c;解锁分布式存储的无限可能。参与者不仅能提升技能&#xff0c;还能通过完成任务赢取丰厚奖励&#xff01;&#x1f30a; 什么是 Walrus&#xff1f; 数据主权如今正成为越…

git 常用命令 git archive

git archive 是 Git 中用于创建一个包含指定提交或分支中所有文件的归档文件&#xff08;如 .tar 或 .zip&#xff09;的命令。这个命令非常适合用于分发项目快照、备份代码库或导出特定版本的文件。 git archive --formatzip --outputproject.zip HEAD …

Excel 技巧15 - 在Excel中抠图头像,换背景色(★★)

本文讲了如何在Excel中抠图头像&#xff0c;换背景色。 1&#xff0c;如何在Excel中抠图头像&#xff0c;换背景色 大家都知道在PS中可以很容易抠图头像&#xff0c;换背景色&#xff0c;其实Excel中也可以抠简单的图&#xff0c;换背景色。 ※所用头像图片为百度搜索&#x…

持续升级《在线写python》小程序的功能,文章页增加一键复制功能,并自动去掉html标签

增加复制按钮后的界面是这样的 代码如下&#xff1a; <template><view><x-header></x-header><view class"" v-if"article_info"><view class"kuai bgf"><view class"ac fs26"><img sr…

FPGA与ASIC:深度解析与职业选择

IC&#xff08;集成电路&#xff09;行业涵盖广泛&#xff0c;涉及数字、模拟等不同研究方向&#xff0c;以及设计、制造、封测等不同产业环节。其中&#xff0c;FPGA&#xff08;现场可编程门阵列&#xff09;和ASIC&#xff08;专用集成电路&#xff09;是两种重要的芯片类型…

【Linux】Linux入门(三)权限

目录 前提权限概念whoami指令 Linux权限管理文件访问者的分类&#xff08;人&#xff09;file指令权限信息权限的表示方法 chmod指令 更改权限chown指令 修改文件&#xff0c;文件夹所属用户和用户组 权限掩码umask&#xff08;权限掩码&#xff09; 粘滞位 前提 请先看下面这…

蓝桥与力扣刷题(73 矩阵置零)

题目&#xff1a;给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]]示例 2&…

Node.js接收文件分片数据并进行合并处理

前言&#xff1a;上一篇文章讲了如何进行文件的分片&#xff1a;Vue3使用多线程处理文件分片任务&#xff0c;那么本篇文章主要看一下后端怎么接收前端上传来的分片并进行合并处理。 目录&#xff1a; 一、文件结构二、主要依赖1. express2. multer3. fs (文件系统模块)4. pat…

大数据,Hadoop,HDFS的简单介绍

大数据 海量数据&#xff0c;具有高增长率、数据类型多样化、一定时间内无法使用常规软件工具进行捕捉、管理和处理的数据集 合 大数据的特征: 4V Volume : 巨大的数据量 Variety : 数据类型多样化 结构化的数据 : 即具有固定格式和有限长度的数据 半结构化的数据 : 是…

深度强化学习:PPO

深度强化学习算法&#xff1a;PPO 1. Importance Sampling 先说一下什么是采样&#xff1a;对于一个随机变量&#xff0c;我们通常用概率密度函数来描述该变量的概率分布特性。具体来说&#xff0c;给定随机变量的一个取值&#xff0c;可以根据概率密度函数来计算该值对应的概…

Flink底层架构与运行流程

这张图展示了Flink程序的架构和运行流程。 主要组件及功能&#xff1a; Flink Program&#xff08;Flink程序&#xff09;&#xff1a; 包含Program code&#xff08;程序代码&#xff09;&#xff0c;这是用户编写的业务逻辑代码。经过Optimizer / Graph Builder&#xff08…

嵌入式知识点总结 C/C++ 专题提升(一)-关键字

针对于嵌入式软件杂乱的知识点总结起来&#xff0c;提供给读者学习复习对下述内容的强化。 目录 1.C语言宏中"#“和"##"的用法 1.1.(#)字符串化操作符 1.2.(##)符号连接操作符 2.关键字volatile有什么含意?并举出三个不同的例子? 2.1.并行设备的硬件寄存…

mysql精简单机版,免登录,可复制,不启动服务与本机mysql无冲突

突然有了个需要在本地使用的mysql需求&#xff0c;要求不用安装,随拷随用,不影响其他mysql服务,占用空间小.基于这种需求做了个精简版的mysql 首先下载mysql的zip安装包 > windows 64位 > https://repo.huaweicloud.com/mysql/Downloads/MySQL-5.7/mysql-5.7.36-winx64…

俄语画外音的特点

随着全球媒体消费的增加&#xff0c;语音服务呈指数级增长。作为视听翻译和本地化的一个关键方面&#xff0c;画外音在确保来自不同语言和文化背景的观众能够以一种真实和可访问的方式参与内容方面发挥着重要作用。说到俄语&#xff0c;画外音有其独特的特点、挑战和复杂性&…

【vitePress】基于github快速添加评论功能(giscus)

一.添加评论插件 使用giscus来做vitepress 的评论模块&#xff0c;使用也非常的简单&#xff0c;具体可以参考&#xff1a;giscus 文档&#xff0c;首先安装giscus npm i giscus/vue 二.giscus操作 打开giscus 文档&#xff0c;如下图所示&#xff0c;填入你的 github 用户…

python麻辣香锅菜品推荐

1.推荐算法概述 推荐算法出现得很早,最早的推荐系统是卡耐基梅隆大学推出的Web Watcher浏览器导航系统&#xff0c;可以根据当的搜索目标和用户信息,突出显示对用户有用的超链接。斯坦福大学则推出了个性化推荐系统LIRA.AT&T实验室于1997年提出基于协作过滤的个性化推荐系统…

Android系统开发(六):从Linux到Android:模块化开发,GKI内核的硬核科普

引言&#xff1a; 今天我们聊聊Android生态中最“硬核”的话题&#xff1a;通用内核镜像&#xff08;GKI&#xff09;与内核模块接口&#xff08;KMI&#xff09;。这是内核碎片化终结者的秘密武器&#xff0c;解决了内核和供应商模块之间无尽的兼容性问题。为什么重要&#x…

UE 像素流Pixel Streaming笔记

参考 UE 像素流Pixel Streaming基本介绍和使用方法 UE4-PixelStreaming&#xff08;虚幻引擎4-像素流&#xff09;笔记 UE 像素流常用回调 UE 像素流通信 这链接能学到不少像素流的东西 使用 1.像素流连接成功&#xff08;On New Connection&#xff09; 必须使用GetPixe…