go基础08-map的内部实现

news2024/10/1 5:41:17

和切片相比,map类型的内部实现要复杂得多。Go运行时使用一张哈希表来实现抽象的map类型。运行时实现了map操作的所有功能,包括查找、插入、删除、遍历等。在编译阶段,Go编译器会将语法层面的map操作重写成运行时对应的函数调用。

下面是大致的对应关系:

// $GOROOT/src/cmd/compile/internal/gc/walk.go
// $GOROOT/src/runtime/map.go
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
v := m["key"] → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"] → v, ok := runtime.mapaccess2(maptype, m, "key")
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") // v是用于后续存储value 的空间的地址
delete(m, "key") → runtime.mapdelete(maptype, m, "key")

下图是map类型在运行时层实现的示意图。

在这里插入图片描述

1. 初始状态

从图上中我们可以看到,与语法层面map类型变量一一对应的是runtime.hmap类型的实例。hmap是map类型的header,可以理解为map类型的描述符,它存储了后续map类型操作所需的所有信息。

● count:当前map中的元素个数;对map类型变量运用len内置函数时,len函数返回的就是count这个值。

● flags:当前map所处的状态标志,目前定义了4个状态值——iterator、oldIterator、hashWriting和sameSizeGrow。

● B:B的值是bucket数量的以2为底的对数,即2^B = bucket数量。

● noverflow:overflow bucket的大约数量。

● hash0:哈希函数的种子值。

● buckets:指向bucket数组的指针。

● oldbuckets:在map扩容阶段指向前一个bucket数组的指针。

● nevacuate:在map扩容阶段充当扩容进度计数器。所有下标号小于nevacuate的bucket都已经完成了数据排空和迁移操作。

● extra:可选字段。如果有overflow bucket存在,且key、value都因不包含指针而被内联(inline)的情况下,该字段将存储所有指向overflow bucket的指针,保证overflow bucket是始终可用的(不被垃圾回收掉)。

真正用来存储键值对数据的是bucket(桶),每个bucket中存储的是Hash值低bit位数值相同的元素,默认的元素个数为BUCKETSIZ(值为8,在$GOROOT/src/cmd/compile/internal/gc/reflect.go中定义,与runtime/map.go中常量bucketCnt保持一致)。

当某个bucket(比如buckets[0])的8个空槽(slot)都已填满且map尚未达到扩容条件时,运行时会建立overflow bucket,并将该overflow bucket挂在上面bucket(如buckets[0])末尾的overflow指针上,这样两个bucket形成了一个链表结构,该结构的存在将持续到下一次map扩容。

每个bucket由三部分组成:tophash区域、key存储区域和value存储区域。

1)tophash区域

当向map插入一条数据或从map按key查询数据的时候,运行时会使用哈希函数对key做哈希运算并获得一个哈希值hashcode。这个hashcode非常关键,运行时将hashcode“一分为二”地看待,其中低位区的值用于选定bucket,高位区的值用于在某个bucket中确定key的位置。这个过程可参考下图。

在这里插入图片描述

因此,每个bucket的tophash区域是用于快速定位key位置的,这样避免了逐个key进行比较这种代价较大的操作,尤其是当key是size较大的字符串类型时,这是一种以空间换时间的思路。

2)key存储区域

tophash区域下面是一块连续的内存区域,存储的是该bucket承载的所有key数据。

运行时在分配bucket时需要知道key的大小。那么运行时是如何知道key的大小的呢?当我们声明一个map类型变量时,比如var m map[string]int,Go运行时就会为该变量对应的特定map类型生成一个runtime.maptype实例(如存在,则复用):

// $GOROOT/src/runtime/type.go
type maptype struct {
typ _type
key *_type
elem *_type
bucket *_type // 表示hash bucket的内部类型
keysize uint8 // key的大小
elemsize uint8 // elem的大小
bucketsize uint16 // bucket的大小
flags uint32
}

该实例包含了我们所需的map类型的所有元信息。前面提到过编译器会将语法层面的map操作重写成运行时对应的函数调用,这些运行时函数有一个共同的特点:

第一个参数都是maptype指针类型的参数。Go运行时就是利用maptype参数中的信息确定key的类型和大小的,map所用的hash函数也存放在maptype.key.alg.hash(key, hmap.hash0)中。

同时maptype的存在也让Go中所有map类型共享一套运行时map操作函数,而无须像C++那样为每种map类型创建一套map操作函数,从而减少了对最终二进制文件空间的占用。

运行该程序:

$go run map_concurrent_read_and_write.go
fatal error: concurrent map iteration and map write

我们会得到上述panic信息。如果仅仅是并发读,则map是没有问题的。

Go 1.9版本中引入了支持并发写安全的sync.Map类型,可以用来在并发读写的场景下替换掉map。另外考虑到map可以自动扩容,map中数据元素的value位置可能在这一过程中发生变化,因此Go不允许获取map中value的地址,这个约束是在编译期间就生效的。示例
代码如下:

p := m[key] // 无法获取m[key]的地址
fmt.Println(p)

尽量使用cap参数创建map

从上面的自动扩容原理我们了解到,如果初始创建map时没有创建足够多可以应付map使用场景的bucket,那么随着插入map元素数量的增多,map会频繁扩容,而这一过程将降低map的访问性能。

因此,如果可能的话,我们最好对map使用规模做出粗略的估算,并使
用cap参数对map实例进行初始化。下面是使用cap参数与不使用map参数的map写性能基准测
试及测试结果:


const mapSize = 10000
func BenchmarkMapInitWithoutCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		m := make(map[int]int)
	for i := 0; i < mapSize; i++ {
		m[i] = i
	}
	}
}
func BenchmarkMapInitWithCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		m := make(map[int]int, mapSize)
	for i := 0; i < mapSize; i++ {
		m[i] = i
	}
}
}

可以看出,使用cap参数的map实例的平均写性能是不使用cap参数的2倍。

goos: darwin
goarch: amd64
BenchmarkMapInitWithoutCap-8 2000 645946 ns/op 687188 B/op 276 allocs/op
BenchmarkMapInitWithCap-8 5000 317212 ns/op 322243 B/op 11 allocs/op
PASS
ok command-line-arguments 2.987s

和切片一样,map是Go语言提供的重要数据类型,也是Gopher日常编码中最常使用的类型之一。通过本条的学习我们掌握了map的基本操作和运行时实现原理,并且我们在日常使

用map的场合要把握住下面几个要点:

● 不要依赖map的元素遍历顺序;

● map不是线程安全的,不支持并发写;

● 不要尝试获取map中元素(value)的地址;

● 尽量使用cap参数创建map,以提升map平均访问性能,减少频繁扩容带来的不必要损耗

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

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

相关文章

07-垃圾收集算法详解

上一篇&#xff1a;06-JVM对象内存回收机制深度剖析 1.分代收集理论 当前虚拟机的垃圾收集都采用分代收集算法&#xff0c;这种算法没有什么新的思想&#xff0c;只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代&#xff0c;这样我们就可以根据各…

【LeetCode-中等题】17. 电话号码的字母组合

文章目录 题目方法一&#xff1a;递归回溯 题目 方法一&#xff1a;递归回溯 参考讲解&#xff1a;还得用回溯算法&#xff01;| LeetCode&#xff1a;17.电话号码的字母组合 首先可以画出树图&#xff1a; 先将数字对应的字符集合 加入到一个map集合 这里需要一个index来控…

【vue2第十七章】VueRouter 编程式导航跳转传参(点击按钮跳转路由和如何传递参数)

如何在js进行跳转路由 在一些需求中&#xff0c;我们需要不用点击a标签或者router-link&#xff0c;但是也要实现路由跳转&#xff0c;比如登陆&#xff0c;点击按钮搜索跳转。那么这种情况如何进行跳转呢&#xff1f; 直接再按钮绑定的方法中写this.$router.push(路由路径)即…

软件兼容性测试怎么做?对软件产品起到什么作用?

软件兼容性测试是一项重要的软件测试活动&#xff0c;它可以确保在不同操作系统、硬件配置和软件环境下&#xff0c;软件能够正常运行&#xff0c;并与其他相关软件和系统进行正确的互动。 一、软件兼容性的测试方法 1、操作系统测试&#xff1a;测试软件在不同操作系统上的兼…

linux常用命令及解释大全(二)

目录 前言 一、文件的权限 二、文件的特殊属性 三、打包和压缩文件 四、查看文件内容 五、文本处理 5.1 grep 5.2 sed 5.3 其它 总结 前言 本篇文章接linux常用命令及解释大全&#xff08;一&#xff09;继续介绍了一部分linux常用命令&#xff0c;包括文件的权限&a…

Android签名查看

查看签名文件信息 第一种方法&#xff1a; 1.打开cmd&#xff0c;执行keytool -list -v -keystore xxx.keystore&#xff0c;效果如下图&#xff1a; 第二种方法: 1.打开cmd&#xff0c;执行 keytool -list -v -keystore xxxx.keystore -storepass 签名文件密码&#xff0…

Linux 下 C语言版本的线程池

目录 1. 线程池引入 2. 线程池介绍 3. 线程池的组成 4. 任务队列 5. 线程池定义 6. 头文件声明 7. 函数实现 8. 测试代码 1. 线程池引入 我们使用线程的时候就去创建一个线程&#xff0c;这样实现起来非常简便&#xff0c;但是就会有一个问题&#xff1a;如果并发的线程数…

壁炉在文学和艺术中的代表着什么呢,能给我们带来什么样的影响?

壁炉&#xff0c;不仅仅是家庭温暖的来源&#xff0c;也是文学和艺术中常见的重要元素。它的形象在文学作品、绘画和电影中频繁出现&#xff0c;不仅为故事情节提供了背景&#xff0c;还象征着情感、温馨和安全感。让我们一起深入探讨壁炉在文学和艺术中的形象&#xff0c;以及…

Android相机调用-CameraX【外接摄像头】【USB摄像头】

Android相机调用有原生的Camera和Camera2&#xff0c;我觉得调用代码都太复杂了&#xff0c;CameraX调用代码简洁很多。 说明文档&#xff1a;https://developer.android.com/jetpack/androidx/releases/camera?hlzh-cn 现有查到的调用资料都不够新&#xff0c;对于外接摄像…

RecyclerView的item布局预览显示是一行两块 运行后显示了一行一块,怎么回事

QQ有人问我了&#xff1a; 没有起作用&#xff1f;有 解决问题&#xff1a;在item布局上第一个外层宽度、高度分别为match_parent和wrap_content&#xff0c;第二个外层宽度和高度都为match_parent&#xff0c;运行后可以显示一行两块 其他学习资料&#xff1a; 1、付费专栏…

mybatis 数据库字段加密

目录 1、引入依赖 2、添加配置 3、指定加密字段 4、测试&#xff0c;效果 1、引入依赖 <dependency><groupId>io.github.whitedg</groupId><artifactId>mybatis-crypto-spring-boot-starter</artifactId><version>1.2.3</version>…

C#文件拷贝工具

目录 工具介绍 工具背景 4个文件介绍 CopyTheSpecifiedSuffixFiles.exe.config DataSave.txt 拷贝的存储方式 文件夹介绍 源文件夹 目标文件夹 结果 使用 *.mp4 使用 *.* 重名时坚持拷贝 可能的报错 C#代码如下 Form1.cs Form1.cs设计 APP.config Program.c…

微信小程序踩坑记录

1、原生微信小程序开发&#xff0c;切换版本后一直报错&#xff0c;切回去还是报错&#xff0c;后来排查发现是编辑器未更新的问题 2、本地开发静态时&#xff0c;要把这个勾选&#xff0c;否则每次提交代码都会冲突 持续记录踩坑。。。

果粉崩溃!Icloud又要涨价?我来教你使用群晖生态将本地SSD“上云“!

文章目录 前言本教程解决的问题是&#xff1a;按照本教程方法操作后&#xff0c;达到的效果是想使用群晖生态软件&#xff0c;就必须要在服务端安装群晖系统&#xff0c;具体如何安装群晖虚拟机请参考&#xff1a; 1. 安装并配置synology drive1.1 安装群辉drive套件1.2 在局域…

Android离线文字识别-tesseract4android调用

Android在线文字识别可以调阿里云的接口Android文字识别-阿里云OCR调用__花花的博客-CSDN博客 需要离线文字识别的话&#xff0c;可以调tesseract4android。个人测试效果不是特别理想&#xff0c;但是速度真的很快&#xff0c;VIVO S10后摄照片&#xff0c;80ms内识别完成。现…

手写Spring:第13章-把AOP扩展到Bean的生命周期

文章目录 一、目标&#xff1a;把AOP扩展到Bean的生命周期二、设计&#xff1a;把AOP扩展到Bean的生命周期三、实现&#xff1a;把AOP扩展到Bean的生命周期3.1 工程结构3.2 AOP动态代理融入Bean的生命周期类图3.3 定义Advice拦截器链3.3.1 定义拦截器链接口3.3.2 方法拦截器链接…

算法笔记:堆

【如无特别说明&#xff0c;皆为最小二叉堆】 1 介绍 2 特性 结构性&#xff1a;符合完全二叉树的结构有序性&#xff1a;满足父节点小于子节点&#xff08;最小化堆&#xff09;或父节点大于子节点&#xff08;最大化堆&#xff09; 3 二叉堆的存储 顺序存储 二叉堆的有序…

问道管理:a股交易时间和规则?

A股是指中国境内流通的人民币普通股票&#xff0c;是国内投资者最为熟悉和广泛的投资工具之一。作为投资者&#xff0c;了解A股的买卖时刻和规矩是非常重要的。本文从买卖时刻、买卖规矩、买卖方式等多个视点来分析A股买卖时刻和规矩&#xff0c;期望对读者有所协助。 A股买卖…

C#事件event

事件模型的5个组成部分 事件拥有者&#xff08;event source&#xff09;&#xff08;类对象&#xff09;&#xff08;有些书将其称为事件发布者&#xff09; 事件成员&#xff08;event&#xff09;&#xff08;事件拥有者的成员&#xff09;&#xff08;事件成员就是事件本身…

2023年03月 C/C++(八级)真题解析#中国电子学会#全国青少年软件编程等级考试

C/C编程&#xff08;1~8级&#xff09;全部真题・点这里 第1题&#xff1a;最短路径问题 平面上有n个点&#xff08;n<100&#xff09;&#xff0c;每个点的坐标均在-10000~10000之间。其中的一些点之间有连线。 若有连线&#xff0c;则表示可从一个点到达另一个点&#xff…