Golang中使用map时注意的问题

1. 将value定义为struct节省内存

1. 消除指针引用

map 的 value 是 struct 类型时,数据会直接存储在 map 中,而不是通过指针引用。这可以减少内存分配的开销和 GC(垃圾回收)的负担。

type User struct {ID   intName string
}m := make(map[string]User)
m["user1"] = User{ID: 1, Name: "John"}// Example with pointer to struct
m2 := make(map[string]*User)
m2["user1"] = &User{ID: 1, Name: "John"}

在第二个示例中,map 中存储的是指向 User 结构体的指针,这意味着除了存储指针本身外,还需要额外的内存来存储 User 结构体,并且会增加 GC 的负担。

2. 避免内存碎片化

存储指针时,由于指针可能指向堆中的不同位置,这会导致内存碎片化,增加了内存使用的不确定性。而存储 struct 使得数据更紧凑,减少了碎片化。

3. 更高的缓存命中率

由于 struct 的数据是紧凑存储的,相对于存储指针,struct 的数据更可能在相邻的内存位置。这增加了 CPU 缓存的命中率,从而提高了性能。

示例:节约内存

下面是一个示例,展示了如何通过定义 struct 类型来节约内存:

package mainimport ("fmt""runtime"
)type User struct {ID   intName string
}func main() {// 使用 struct 作为 valueusers := make(map[string]User)for i := 0; i < 1000000; i++ {users[fmt.Sprintf("user%d", i)] = User{ID: i, Name: fmt.Sprintf("Name%d", i)}}printMemUsage("With struct values")// 使用指针作为 valueuserPtrs := make(map[string]*User)for i := 0; i < 1000000; i++ {userPtrs[fmt.Sprintf("user%d", i)] = &User{ID: i, Name: fmt.Sprintf("Name%d", i)}}printMemUsage("With pointer values")
}func printMemUsage(label string) {var m runtime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("%s: Alloc = %v MiB\n", label, bToMb(m.Alloc))
}func bToMb(b uint64) uint64 {return b / 1024 / 1024
}

4. set实现对比

map[int]bool{}

在这种情况下,map 的 value 类型是 bool。每个键会占用一个 bool 类型的空间(通常是一个字节)。

set := make(map[int]bool)
set[1] = true
set[2] = true

map[int]struct{}{}

在这种情况下,map 的 value 类型是空的 struct。空的 struct 不占用任何内存,因此每个键只占用键本身的内存。

set := make(map[int]struct{})
set[1] = struct{}{}
set[2] = struct{}{}

内存使用对比

map[int]bool{} 会比 map[int]struct{}{} 使用更多的内存,因为 bool 类型需要存储一个字节(在实际应用中可能会有额外的内存对齐和管理开销),而 struct{} 是空的,不会增加任何内存开销。

示例代码对比内存使用

以下是一个示例代码,比较这两种 map 类型的内存使用情况:

package mainimport ("fmt""runtime"
)func main() {// 使用 bool 作为 valueboolMap := make(map[int]bool)for i := 0; i < 1000000; i++ {boolMap[i] = true}printMemUsage("With bool values")// 使用 struct 作为 valuestructMap := make(map[int]struct{})for i := 0; i < 1000000; i++ {structMap[i] = struct{}{}}printMemUsage("With struct values")
}func printMemUsage(label string) {var m runtime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("%s: Alloc = %v MiB\n", label, bToMb(m.Alloc))
}func bToMb(b uint64) uint64 {return b / 1024 / 1024
}

结果

运行上述代码,你会发现使用 struct 作为 value 的内存使用量明显小于使用指针作为 value 的内存使用量。这是因为:

  1. 减少了指针的存储开销
  2. 减少了额外的堆内存分配
  3. 降低了 GC 的负担,因为 struct 的内存管理更简单,不涉及指针的追踪和回收。

2. 哈希分桶的结构

1. 哈希计算

当我们向map中插入一个键值对,首先对键进行哈希计算。Go内置了哈希函数来计算键的哈希值。哈希值是一个64位的整数。

2. 分桶依据

Go 中的 map 是分成多个桶 (bucket) 来存储的。桶的数量通常是 2 的幂次,这样可以方便地通过位运算来定位到具体的桶。哈希值的高八位和低八位分别用于分桶和桶内定位:

  • 高八位 (top 8 bits):用于决定哈希表中的桶位置。
  • 低八位 (low 8 bits):用于桶内查找。

3. 桶 (Bucket) 结构

每个桶中可以存储 8 个键值对。当某个桶中的元素超过 8 个时,Go 会使用溢出桶来存储额外的键值对。桶的结构如下:

type bmap struct {tophash [bucketCnt]uint8keys    [bucketCnt]keyTypevalues  [bucketCnt]valueTypeoverflow *bmap
}

tophash:存储键的哈希值的高八位。

keys:存储键。

values:存储对应的值。

overflow:指向溢出桶的指针。

. 插入过程

当插入一个键值对时,过程如下:

  1. 计算哈希值:对键进行哈希计算得到哈希值 hash
  2. 定位桶:通过 hash >> (64 - B)B 是桶的数量的对数)得到桶的索引 index
  3. 桶内查找:通过 hash & (bucketCnt - 1) 得到桶内索引。然后通过对比 tophash 数组中的值来定位到具体的键值对存储位置。
  4. 存储键值对:将键值对存储到相应的位置,如果当前桶已满,则分配新的溢出桶来存储额外的键值对。

5. 查找过程

查找的过程与插入类似:

  1. 计算哈希值:对键进行哈希计算得到哈希值 hash
  2. 定位桶:通过 hash >> (64 - B) 得到桶的索引 index
  3. 桶内查找:通过 hash & (bucketCnt - 1) 得到桶内索引,然后在相应的 bmap 中查找 tophashkeys 数组中匹配的键。如果在当前桶中没有找到,则继续查找溢出桶。

3. map扩容过程

1. 扩容触发条件

扩容通常在以下两种情况下触发:

  1. 装载因子过高:装载因子(load factor)是 map 中元素数量与桶数量的比值。Go 语言中的装载因子阈值通常为 6.5,当装载因子超过这个值时会触发扩容。
  2. 溢出桶过多:当溢出桶的数量过多时,也会触发扩容。

2. 扩容过程的具体步骤

  1. 初始化新的桶数组: 在需要扩容时,Go 会分配一个新的桶数组,其大小通常是旧桶数组的两倍,并设置相关的元数据以指示 map 正在进行扩容。
  2. 标记迁移状态: 在 map 的内部结构中,会有一个标志位(rehash index)指示当前已经迁移的桶位置。初始值为 0。
  3. 迁移部分数据: 在每次对 map 进行插入或查找操作时,会顺便迁移一部分旧桶中的数据到新桶中。每次迁移一个或多个桶,具体数量取决于操作的复杂度。
  4. 更新 rehash index: 迁移完成后,更新 rehash index,以便下次操作继续迁移下一个桶中的数据。
  5. 完成扩容: 当所有旧桶的数据都迁移到新桶后,更新 map 的元数据,指向新的桶数组,并将扩容状态标志位清除。

4. recovermap的panic

panicrecover 的工作机制

  1. panic
    • panic 用于引发一个恐慌,通常在遇到无法恢复的严重错误时使用。
    • panic 被调用时,程序的正常执行流程会被中断,并开始沿着调用栈向上展开,逐层调用函数的 defer 语句,直到遇到 recover 或者程序崩溃。
  2. recover
    • recover 用于恢复程序的正常执行,通常在 defer 函数中调用。
    • 如果在 defer 语句中调用了 recover,并且当前栈帧处于恐慌状态,那么 recover 会捕获这个恐慌,停止栈的展开,并返回传给 panic 的值。
    • 如果不在恐慌状态下调用 recover,它会返回 nil,不做任何处理。

在 Go 语言中,panicrecover 是用来处理异常情况和错误恢复的两种机制。理解它们的工作原理对于编写健壮的 Go 代码非常重要。以下是对 panicrecover 机制的详细解释以及它们在 map 中的应用。

panicrecover 的工作机制

  1. panic
    • panic 用于引发一个恐慌,通常在遇到无法恢复的严重错误时使用。
    • panic 被调用时,程序的正常执行流程会被中断,并开始沿着调用栈向上展开,逐层调用函数的 defer 语句,直到遇到 recover 或者程序崩溃。
  2. recover
    • recover 用于恢复程序的正常执行,通常在 defer 函数中调用。
    • 如果在 defer 语句中调用了 recover,并且当前栈帧处于恐慌状态,那么 recover 会捕获这个恐慌,停止栈的展开,并返回传给 panic 的值。
    • 如果不在恐慌状态下调用 recover,它会返回 nil,不做任何处理。

map 中使用 panicrecover

在 Go 的 map 中,某些操作(如并发读写未加锁的 map)会引发 panic。这些 panic 可以被 recover 捕获和处理,以防止程序崩溃。

package mainimport ("fmt"
)func main() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered from panic:", r)}}()// 创建一个 mapm := make(map[string]string)// 引发 panic 的操作causePanic(m)fmt.Println("This line will be executed because panic was recovered.")
}func causePanic(m map[string]string) {// 这里尝试并发访问 map,可能会引发 panic// 模拟并发问题,直接引发 panicpanic("simulated map access panic")
}

5. map是如何检测到自己处于竞争状态

在 Go 语言中,map 的竞争状态(concurrent access)指的是多个 goroutine 同时读写同一个 map 而没有适当的同步保护。Go 内置的 map 类型在并发读写时会引发 panic,以防止数据竞争和未定义行为。这种检测主要是通过 Go 编译器和运行时的实现来完成的,而不是底层硬件直接支持的功能

竞争检测机制

  1. 编译器插桩
    • 在编译时,Go 编译器会在对 map 进行读写操作的代码位置插入特定的检测代码。这些检测代码在运行时检查 map 是否处于并发访问状态。
  2. 运行时检查
    • 运行时的检测代码会追踪 map 的访问。当检测到多个 goroutine 同时对 map 进行读写操作时,会引发 panic。具体来说,Go 运行时会记录每个 map 的访问情况,如果检测到并发访问没有通过同步机制(如 sync.Mutex),就会引发 panic。
package mainimport ("fmt""sync"
)func main() {m := make(map[int]int)var wg sync.WaitGroupvar mu sync.Mutex// 启动多个 goroutine 并发写 map,未加锁保护会引发 panicfor i := 0; i < 10; i++ {wg.Add(1)go func(i int) {defer wg.Done()// 取消注释以下行,查看未加锁保护的并发写操作// m[i] = i// 使用互斥锁保护并发写操作mu.Lock()m[i] = imu.Unlock()}(i)}wg.Wait()// 打印 map 内容mu.Lock()for k, v := range m {fmt.Printf("key: %d, value: %d\n", k, v)}mu.Unlock()
}

6. sync.Map和map加锁的区别

  1. 使用场景
    • sync.Map 适用于读多写少的并发场景,简单且高效。
    • 使用 sync.Mutexsync.RWMutex 保护普通 map 适用于需要复杂并发控制或写操作较多的场景。
  2. 性能
    • sync.Map 在读多写少的情况下性能优越,但在写操作频繁时性能可能不如使用互斥锁保护的普通 map。
    • 使用 sync.Mutexsync.RWMutex 可以在读写操作间提供更好的性能平衡,尤其是在写操作较多时。
  3. 复杂性
    • sync.Map 封装了并发控制,使用简单,不需要手动加锁。
    • 使用 sync.Mutexsync.RWMutex 需要手动加锁解锁,代码相对复杂,但更灵活。
  4. 方法支持
    • sync.Map 提供了一些特殊的方法(如 LoadOrStoreRange),方便特定场景下的使用。
    • 使用 sync.Mutexsync.RWMutex 保护的普通 map 可以自由定义自己的方法,更灵活,但需要更多的代码。

最后给大家推荐一个LinuxC/C++高级架构系统教程的学习资源与课程,可以帮助你有方向、更细致地学习C/C++后端开发,具体内容请见 https://xxetb.xetslk.com/s/1o04uB

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

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

相关文章

番外篇 | YOLOv8改进之利用轻量化卷积PConv引入全新的结构CSPPC来替换Neck网络中的C2f | 模型轻量化

前言:Hello大家好,我是小哥谈。本文使用轻量化卷积PConv替换Neck中C2f模块中Bottleneck里的传统卷积核得到CSPPC模块,使得模型更加轻量化。🌈 目录 🚀1.基础概念 🚀2.网络结构 🚀3.添加步骤 🚀4.改进方法 🍀🍀步骤1:block.py文件修改 🍀🍀步…

导航定位程序编译调试经验【1-20】

https://github.com/LiZhengXiao99/Navigation-Debug记录一些导航程序编译调试过程中遇到的问题&#xff0c;和我找到的解决方案&#xff0c;以后遇到啥问题了&#xff0c;都来记录一下&#xff1b;如果针对我提出的问题&#xff0c;您有更好的解决方案&#xff0c;欢迎评论分享…

MySQL 8 命令安装卸载教程

一、下载MySQL8 下载连接 MySQL :: Download MySQL Community Server 我下载的是当前最新版8.4 二、安装 1.解压 解压到需要安装的位置&#xff0c;例如我的位置&#xff1a; 2.创建配置文件 新建文本文档&#xff0c;复制下面配置文件&#xff08;注意修改路经&#xff09;…

Modbus TCP什么场景用?

什么是Modbus TCP Modbus TCP是一种基于TCP/IP网络的通信协议&#xff0c;它允许不同的设备通过以太网进行数据交换。Modbus协议最初是为串行通信设计的&#xff0c;但随着网络技术的发展&#xff0c;Modbus TCP应运而生&#xff0c;它继承了Modbus RTU和Modbus ASCII的许多优点…

平凉小果子,平凡中的惊艳味道

平凉美食小果子&#xff0c;这看似平凡的名字背后&#xff0c;藏着无数平凉人的美好回忆。它不仅仅是一种食物&#xff0c;更是一种情感的寄托&#xff0c;一种文化的传承。小果子的制作过程看似简单&#xff0c;实则蕴含着深厚的功夫。选用优质的面粉作为主要原料&#xff0c;…

Java学习笔记(多线程):CompetableFuture

本文是自己的学习笔记&#xff0c;主要参考资料如下 https://www.cnblogs.com/dolphin0520/p/3920407.html JavaSE文档 https://blog.csdn.net/ThinkWon/article/details/102508721 1、Overview2、重要参数3、主要方法3.1、创建实例&#xff0c;获取返回值3.2、线程执行顺序相关…

std::trhead的回调,中频繁发送信号,会导致qt的事件循环处理不过来吗

在Qt中,事件循环是负责处理所有事件和信号的核心机制。事件循环会不断地检查是否有待处理的事件,并且调度相应的事件处理器。在标准模板库(STL)的std::thread中使用回调函数来频繁发送信号到Qt的事件循环中,确实可能会导致性能问题,尤其是在高频率信号发送的情况下。 当…

大自然高清风景视频无水印素材在哪下载?下载视频素材网分享

在视频创作领域&#xff0c;一段高清的风景视频可以极大地提升你的作品质感。无论是作为背景、过渡片段还是主要内容&#xff0c;优质的风景视频素材都是必不可少的。然而&#xff0c;寻找既高清又无水印的风景视频素材并非易事。为了帮助大家轻松获取这类素材&#xff0c;我整…

牛客周赛 Round 48 解题报告 | 珂学家

前言 题解 这场感觉有点难&#xff0c;D完全没思路, EF很典&#xff0c;能够学到知识. E我的思路是容斥贡献&#xff0c;F很典&#xff0c;上周考过一次&#xff0c;引入虚拟节点质数(有点像种类并查集类似的技巧). 欢迎关注 珂朵莉 牛客周赛专栏 珂朵莉 牛客小白月赛专栏 …

Marin说PCB之total etch length规则知多少?

魔都上海最近迎来了一轮梅雨季节了&#xff0c;小编我上周就已经提前把被子衣服袜子都晒了一遍&#xff0c;省的后面一段时间下雨就不能晒了。这种阴雨绵绵的天气当然在家里睡觉最舒服了&#xff0c;上周留正当我在家里夏眠的时候&#xff0c;突然被一阵手机铃声吵醒了&#xf…

代码签名证书:保护你的软件,就像保护你的宝贝

Hey&#xff0c;大家好&#xff01;今天我们来聊聊一个听起来可能有点技术宅&#xff0c;但实际上超级重要的东西——代码签名证书。别担心&#xff0c;我会用最简单易懂的话来解释它&#xff0c;保证你看完这篇文章后&#xff0c;能对代码签名证书有个全新的认识&#xff01; …

苹果Mac安装adobe软件报错“installer file may be damaged”解决方案

最近Mac电脑系统的有小伙伴在安装PS、AI、AE、PR等软件&#xff0c;出现了一个错误&#xff0c;让人头疼不已&#xff0c;苦苦找寻&#xff0c;也找不到完美的解决方法。让我们来一起看看吧&#xff01; 很多小伙伴都喜欢苹果电脑&#xff0c;但是在安装外来软件时&#xff0c;…

python--os.walk()函数使用(超详细)

在Python 3.7中&#xff0c;os.walk()函数的用法与早期版本&#xff08;包括Python 3.4及之后&#xff09;保持一致。os.walk()是一个用于遍历目录树的生成器函数&#xff0c;它生成给定目录中的文件名。这个函数没有直接的参数&#xff08;除了你要遍历的目录路径&#xff0c;…

Java后端 || ElementUI 显示后端树形表格数据

文章目录 1、前端源码2、数据库设计3、后端设计3.1、实体类3.2、Controller层3.3、具体树形列表后端代码实现 1、前端源码 ElementUI Table 链接 在此链接中找到 树形数据与懒加载 查看其JS源码&#xff0c;可知&#xff0c;每个菜单节点的子节点存放于children字段中&#x…

讲讲js中的prototype和__proto__

在Javascript中&#xff0c;prototype和__proto__是两个重要的概念&#xff0c;在对象的原型链中扮演重要的角色。 prototype prototype是js函数的内置属性&#xff0c;每个函数都有一个prototype属性&#xff0c;它是一个指针&#xff0c;指向一个对象&#xff08;原型对象&a…

信息学奥赛初赛天天练-36-CSP-J2021阅读程序-ASCII、运算符优先级、二进制补码存储、模拟算法应用

PDF文档公众号回复关键字:20240626 2021 CSP-J 阅读程序2 1 阅读程序(判断题1.5分 选择题3分 共计40分 ) #include<stdio.h> #include<string.h>char base[64]; char table[256]; char str[256]; char ans[256];void init() {for(int i0;i<26;i) base[i]Ai;fo…

MySQL之可扩展性(四)

可扩展性 向外扩展 分片?还是不分片&#xff1f; 这是一个问题&#xff0c;对吧&#xff1f;答案很简单:如非必要&#xff0c;尽量不分片。首先看是否能通过性能调优或者更好的应用或数据库设计来推迟分片。如果能足够长时间地推迟分片&#xff0c;也许可以直接购买更大地服…

MySQL添加外键约束经典案例

1DDL建表语句 需要一个emp员工表和一个dept部门表 CREATE TABLE emp (id int NOT NULL AUTO_INCREMENT,name varchar(50) COLLATE utf8mb4_0900_as_ci NOT NULL COMMENT 姓名,age int DEFAULT NULL COMMENT 年龄,job varchar(20) COLLATE utf8mb4_0900_as_ci DEFAULT NULL CO…

黑马程序员——Spring框架——day09——linux初级

目录&#xff1a; 前言 什么是Linux&#xff1f;为什么要学Linux 企业用人要求个人发展需要学完Linux能干什么 1).环境搭建2).常用命令3).安装软件4).项目部署小结2.Linux简介 主流操作系统Linux发展历史Linux系统版本Linux安装 安装方式介绍安装VMware安装Linux网卡设置安装S…

flink 处理函数和流转换

目录 处理函数分类 概览介绍 KeydProcessFunction和ProcessFunction 定时器TimeService 窗口处理函数 多流转换 分流-侧输出流 合流 联合&#xff08;Uniion&#xff09; 连接&#xff08;connect&#xff09; 广播连接流&#xff08;BroadcatConnectedStream&#xf…