go语言map扩容

map是什么?

​在Go语言中,map是一种内置的无序key/value键值对的集合,可以根据key在O(1)的时间复杂度内取到value,有点类似于数组或者切片结构,可以把数组看作是一种特殊的map,数组的key为数组的下标,而map的key可以为任意的可比较结构。在map中key不允许重复并且要能够比较。

在go语言中,map的底层采用hash表,用变种拉链法来解决hash冲突问题。

哈希冲突

基本概念:

哈希冲突(哈希碰撞)是指在使用哈希函数时,两个不同的输入值(称为键或关键字)产生了相同的输出值(哈希码或哈希值)的现象。换句话说,当hash(key₁) = hash(key₂)但key₁ ≠ key₂时,就发生了哈希冲突。

简单来说就是两个不同的键被映射到同一哈希桶

产生原因:

  • 哈希表容量限制:即使哈希函数能够均匀分布哈希值,但是当哈希表容量有限时,通过取模运算(如hash(key) mod size)仍可能导致不同哈希值映射到同一位置。
  • 哈希函数设置:设计不佳的哈希函数可能无法将键均匀分布到哈希空间中,导致某些区域过于集中,会增加冲突概率。
  • 数据分布特性:当输入数据具有特定的分布模式或集中在某个范围内时,经过哈希计算后更容易产生冲突。例如,大量相似的字符串作为键可能导致冲突率升高。

解决哈希冲突一般有两种方式:拉链法和开放寻址法

拉链法

​拉链法是一种最常见的解决哈希冲突的方法,很多语言都是用拉链法哈希冲突。拉链法的主要实现是底层不直接使用连续数组来直接存储数据元素,而是使用通过数组和链表组合连使用,数组里存储的其实是一个指针,指向一个链表。当出现两个key比如key1和key2的哈希值相同的情况,就将数据链接到链表上。拉链法处理冲突简单,可以动态的申请内存,删除增加节点都很方便,当冲突严重,链表长度过长的时候也支持更多的优化策略,比如用红黑树代替链表。

拉链法结构:

在这里插入图片描述

左边为一个连续数组,数组每个元素存储一个指针,指向一个链表,链表里每个节点存储的是发生hash冲突的数据

开放地址法

开放地址法是另外一种非常常用的解决哈希冲突的策略,与拉链法不同,开放地址法是将具体的数据元素存储在数组桶中,在要插入新元素时,先根据哈希函数算出hash值,根据hash值计算索引,如果发现冲突了,计算出的数组索引位置已经有数据了,就继续向后探测,直到找到未使用的数据槽为止。哈希函数可以简单地为:hash(key)=(hash1(key)+i)%len(buckets)

开放地址法结构:

在这里插入图片描述

在存储键值对<b,101>的时候,经过hash计算,发现原本应该存放在数组下标为2的位置已经有值了,存放了<a,100>,就继续向后探测,发现数组下标为3的位置是空槽,未被使用,就将<b,101>存放在这个位置。

Go中Map的底层

go语言中的map其实就是一个指向hmap的指针,占用8个字节。所以map底层结构就是hmap,hmap包含多个结构为bmap的bucket数组,当发生冲突的时候,会到正常桶里面的overflow指针所指向的溢出桶里面去找,Go语言中溢出桶也是一个动态数组形式,它是根据需要动态创建的。Go语言中处理冲突采用了优化的拉链法,链表中每个节点存储的不是一个键值对,而是8个键值对。其整体的结构如下图:

在这里插入图片描述

hmap结构体定义:

// A header for a Go map.
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)extra *mapextra // optional fields
}

结构体中各个字段的含义

字段释义
countmap中元素的个数,对应len(map)的值
flags状态标志位,标记map的一些状态
B桶数以2为底的对数,即B=log2(len(buckets)), 如B = 3,那么桶数为2^3 = 8
noverflow溢出桶数量近似值
hash0哈希种子
buckets指向buckets数组的指针,buckets数组的元素为bmap,如果元素个数为0,其值为nil
oldbuckets是一个指向buckets数组的指针,在扩容时,oldbuckets指向老的buckets数组,非扩容时,oldbuckets为空
nevacuate表示扩容进度的一个计数器,小于该值的桶已经迁移完毕
extra指向mapextra结构的指针,mapextra存储map中的溢出桶

mapextra结构体定义

// mapextra holds fields that are not present on all maps.
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
}

各个字段含义

字段释义
overflow溢出桶链表地址
oldoverflow旧桶使用的溢出桶链表地址
nextoverflow下一个空闲桶地址

hmap中真正用于存储数据的是buckets指向的这个bmap(桶)数组,每一个bmap 都能存储8个键值对,当map中的数据过多,bmap数组存不下的时候就会存储到extra指向的溢出bucket(桶)里面

bmap结构体定义

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

各个字段含义

字段释义
topbits存储了bmap里面8个key/value键值对的每个key根据哈希函数计算出的hash值的高8位
keys存储了bmap里面8个key/value键值对的key
values存储了bmap里面8个key/value键值对的value
overflow指向溢出桶的指针

go语言的map会根据每一个key计算出一个hash值,go中对这个hash值得使用,并不是一次性使用的,而是分开使用的,在使用中将hash按照用途可以分为:高位和低位

在这里插入图片描述

假设对一个key做hash计算得到了一个hash值如图所示,蓝色就是这个hash值的高8位,红色就是这个hash值的低8位。而每个bmap中其实存储的就是8个这个蓝色的数字。

通过上图map的底层结构图可以看到bmap的结构,bmap显示存储了8个tohash值,然后存储了8个键值对注意,这8个键值对并不是按照key/value这样key和value放在一起存储的,而是先连续存完8个key,之后再连续存储8个value这样,当键值对不够8个时,对应位置就留空。这样存储的好处是可以消除字节对齐带来的空间浪费。

map的扩容

​随着不断地往map里写入元素,会导致map的数据量变得很大,hash性能会逐渐变差,而且溢出桶会越来越多,导致查找的性能变得很差。所以,需要更多的桶和更大的内存保证哈希的读写性能,这时map会自动触发扩容,在runtime.mapassign可以看到这条语句:

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {...if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {hashGrow(t, h)goto again}...
}

map在两种情况下会触发扩容

  • 负载因子已经超过6.5:双倍扩容
  • 溢出桶数量过多:等量扩容
什么是负载因子

负载因子 = 哈希表中元素的数量 / 桶的数量

为什么负载因子是6.5

​源码里对负载因子的定义是6.5,是经过测试后取出的一个比较合理的值,每个 bucket 有8个空位,假设map里所有的数组桶都装满元素,没有一个数组有溢出桶,那么这时的负载因子刚好是8。而负载因子是6.5的时候,说明数组桶快要用完了,存在溢出的情况下,查找一个key很可能要去遍历溢出桶会造成查找性能下降,所以有必要扩容了

溢出桶数量过多

​可以想象一下这种情况,先往一个map插入很多元素,然后再删除很多元素?再插入很多元素。会造成什么问题?

​由于插入了很多元素,在不是完全理想的情况下,肯定会创建一些溢出桶,但是,又由于没有达到负载因子的临界值,所以不会触发扩容,这时候再删除很多元素,这个时候负载因子又会减小,再插入很多元素,会继续创建更多的溢出桶,导致查找元素的时候要去遍历很多的溢出桶链表,性能下降。

​所以在这种情况下要进行扩容,新建一个桶数组把原来的数据拷贝到里面,这样数据排列更紧密,查找性能更快。

扩容过程

扩容过程需要用到两个函数,hashGrow()growWork()

扩容函数
func hashGrow(t *maptype, h *hmap) {// If we've hit the load factor, get bigger.// Otherwise, there are too many overflow buckets,// so keep the same number of buckets and "grow" laterally.bigger := uint8(1)if !overLoadFactor(h.count+1, h.B) {bigger = 0h.flags |= sameSizeGrow}oldbuckets := h.bucketsnewbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)flags := h.flags &^ (iterator | oldIterator)if h.flags&iterator != 0 {flags |= oldIterator}// commit the grow (atomic wrt gc)h.B += biggerh.flags = flagsh.oldbuckets = oldbucketsh.buckets = newbucketsh.nevacuate = 0h.noverflow = 0if h.extra != nil && h.extra.overflow != nil {// Promote current overflow buckets to the old generation.if h.extra.oldoverflow != nil {throw("oldoverflow is not nil")}h.extra.oldoverflow = h.extra.overflowh.extra.overflow = nil}if nextOverflow != nil {if h.extra == nil {h.extra = new(mapextra)}h.extra.nextOverflow = nextOverflow}// the actual copying of the hash table data is done incrementally// by growWork() and evacuate().
}

go语言在对map进行过扩容的时候,并不是一次性将map的所有数据从旧的桶搬到新的桶,如果map的数据量很大,会非常影响性能,而是采用一种“渐进式”的数据转移技术,遵循写时复制(copy on write)的规则,每次只对使用到的数据做迁移。

简单分析一下扩容过程:

通过代码分析,hashGrow(函数是在mapassiqn函数中被调用,所以,扩容过程会发生在map的赋值操作,在满足上述两个扩容条件时触发。
扩容过程中大概需要用到两个函数,hashGrow(和growWork()。其中hashGrow()函数只是分配新的 buckets,并将老的 buckets 挂到了 oldbuckets 字段上,并未参与真正的数据迁移,而数据迁移的功能是由growWork()函数完成的。

迁移时机

growWork() 函数会在 mapassign 和 mapdelete 函数中被调用,所以数据的迁移过程一般发生在插入或修改、删除 key 的时候。在扩容完毕后(预分配内存),不会马上就进行迁移。而是采取写时复制的方式,当有访问到具体bukcet 时,才会逐渐的将 oldbucket 迁移到 新bucket中。

growWork0)函数定义如下:

func growWork(t *maptype, h *hmap, bucket uintptr) {// 首先把需要操作的bucket迁移evacuate(t, h, bucket&h.oldbucketmask())// 再顺带迁移一个bucketif h.growing() {evacuate(t, h, h.nevacuate)}
}

分析evacuate函数,大致迁移过程如下:

1.先判断当前bucket是不是已经迁移,没有迁移就做迁移操作

 b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))newbit := h.noldbuckets()// 判断旧桶是否已经被迁移了if !evacuated(b) {do...  // 做转移操作}

2.evacuated函数直接通过tohash中第一个hash值判断当前bucket是否被转移

func evacuated(b *bmap) bool {h := b.tophash[0]return h > emptyOne && h < minTopHash
}

3.数据迁移时,根据扩容规则,可能是迁移到大小相同的buckets上,也可能迁移到2倍大的buckets上。

如果迁移到等量数组上,则迁移完的目标桶位置还是在原先的位置上的。如果是双倍扩容迁移到2倍桶数组上,迁移完的目标桶位置有可能在原位置,也有可能在原位置+偏移量。(偏移量大小为原桶数组的长度)。xy 标记目标迁移位置,x标识的是迁移到相同的位置,y标识的是迁移到2倍桶数组上的位置。

var xy [2]evacDst​
x := &xy[0]​
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))​
x.k = add(unsafe.Pointer(x.b), dataOffset)​
x.e = add(x.k, bucketCnt*uintptr(t.keysize))​
​
if !h.sameSizeGrow() {// Only calculate y pointers if we're growing bigger.​// Otherwise GC can see bad pointers.​y := &xy[1]​y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))​y.k = add(unsafe.Pointer(y.b), dataOffset)​y.e = add(y.k, bucketCnt*uintptr(t.keysize))}

evacDst结构如下:

type evacDst struct {​b *bmap          // 迁移桶​i int            // 迁移桶槽下标​k unsafe.Pointer // 迁移桶Key指针​e unsafe.Pointer // 迁移桶Val指针​
}

4.确定完bucket之后,就会按照bucket内的槽位逐条迁移key/value键值对

5.迁移完一个桶后,迁移标记位nevacuate+1,当nevacuate等于旧桶数组大小时,迁移完成,释放旧的桶数组和旧的溢出桶数组

扩容过程大致如下:

  • 等量扩容:等量扩容,目标桶扩容后还在原位置处

在这里插入图片描述

  • 双倍扩容:目标桶扩容后位置可能在原位置,也可能在原位置+偏移量处
    在这里插入图片描述

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

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

相关文章

2025年SDK游戏盾实战深度解析:防御T级攻击与AI反作弊的终极方案

一、引言&#xff1a;游戏安全的“生死防线” 2025年&#xff0c;全球游戏行业因DDoS攻击日均损失3.2亿元&#xff0c;攻击峰值突破8Tbps&#xff0c;且70% 的攻击为混合型&#xff08;DDoSCC&#xff09;。传统高防IP因延迟高、成本贵、协议兼容性差&#xff0c;已无法满足实…

【Linux】LInux下第一个程序:进度条

前言&#xff1a; 在前面的文章中我们学习了LInux的基础指令 【Linux】初见&#xff0c;基础指令-CSDN博客【Linux】初见&#xff0c;基础指令&#xff08;续&#xff09;-CSDN博客 学习了vim编辑器【Linux】vim编辑器_linux vim insert-CSDN博客 学习了gcc/g【Linux】编译器gc…

Web前端基础

### 一、浏览器 火狐浏览器、谷歌浏览器(推荐)、IE浏览器 推荐谷歌浏览器原因&#xff1a; 1、简洁大方,打开速度快 2、开发者调试工具&#xff08;右键空白处->检查&#xff0c;打开调试模式&#xff09; ### 二、开发工具 核心IDE工具 1. Visual Studio Code (VS Code)‌…

C++调试(肆):WinDBG分析Dump文件汇总

目录 1.前言 2.WinDBG中常用的指令 3.分析异常时要关注的信息 4.心得 前言 本篇博客主要针如何使用WinDBG工具调试Dump文件的流程进行一个讲解&#xff0c;具体捕获的Dump文件也是前两节例子中生成的Dump文件。 WinDBG中常用的指令 关于WinDBG调试时常用的指令主要分为以下几种…

SOC-ESP32S3部分:33-声学前端模型ESP-SR

飞书文档https://x509p6c8to.feishu.cn/wiki/YnbmwtqI5iBwE3kHA7AcZ3yTnLf ESP-SR 是乐鑫官方开发的一个音频组件&#xff0c;支持以下模块&#xff1a; 声学前端算法 AFE唤醒词检测 WakeNet命令词识别 MultiNet语音合成&#xff08;目前只支持中文&#xff09; 组件地址&am…

基于vscode,idea,java,html,css,vue,echart,maven,springboot,mysql数据库,在线考试系统

详细视频&#xff1a;【基于vscode,idea,java,html,css,vue,echart,maven,springboot,mysql数据库&#xff0c;在线考试系统-哔哩哔哩】 https://b23.tv/7hwmwmQ

【Linux】shell中的运行流程控制

目录 一.什么是运行流程控制 二.条件允许流程控制--if 2.1.单分支 2.2.双分支 2.3.多分支 if多分支练习 三.循环运行流程控制 无判定循环--for 判断循环--while&#xff0c;until 四.选择运行流程控制 五.自动应答--expect 5.1.固定位置的交互应答 5.2.非固定位置的…

新能源汽车热管理核心技术解析:冬季续航提升40%的行业方案

新能源汽车热管理核心技术解析&#xff1a;冬季续航提升40%的行业方案 摘要&#xff1a;突破续航焦虑的关键在热能循环&#xff01; &#x1f449; 本文耗时72小时梳理行业前沿方案&#xff0c;含特斯拉/比亚迪等8家车企热管理系统原理图 一、热管理为何成新能源车决胜关键&am…

OCR MLLM Evaluation

为什么需要评测体系&#xff1f;——背景与矛盾 ​​ 能干的事&#xff1a;​​ 看清楚发票、身份证上的字&#xff08;准确率>90%&#xff09;&#xff0c;速度飞快&#xff08;眨眼间完成&#xff09;。​​干不了的事&#xff1a;​​ 碰到复杂表格&#xff08;合并单元…

深入解析JVM工作原理:从字节码到机器指令的全过程

一、JVM概述 Java虚拟机(JVM)是Java平台的核心组件&#xff0c;它实现了Java"一次编写&#xff0c;到处运行"的理念。JVM是一个抽象的计算机器&#xff0c;它有自己的指令集和运行时内存管理机制。 JVM的主要职责&#xff1a; 加载&#xff1a;读取.class文件并验…

Python绘图库及图像类型之特殊领域可视化

Python绘图库及图像类型之基础图表-CSDN博客https://blog.csdn.net/weixin_64066303/article/details/148433762?spm1001.2014.3001.5501 Python绘图库及图像类型之高级可视化-CSDN博客https://blog.csdn.net/weixin_64066303/article/details/148450750?spm1001.2014.3001.…

04 APP 自动化- Appium toast 元素定位列表滑动

文章目录 一、toast 元素的定位二、滑屏操作 一、toast 元素的定位 toast 元素就是简易的消息提示框&#xff0c;toast 显示窗口显示的时间有限&#xff0c;一般3秒左右 # -*- codingutf-8 -*- from time import sleep from appium import webdriver from appium.options.an…

C/C++ OpenCV 矩阵运算

C/C OpenCV 矩阵运算详解 &#x1f4a1; OpenCV 是一个强大的开源计算机视觉和机器学习库&#xff0c;它提供了丰富的矩阵运算功能&#xff0c;这对于图像处理和计算机视觉算法至关重要。本文将详细介绍如何使用 C/C 和 OpenCV 进行常见的矩阵运算。 矩阵的创建与初始化 在进…

基于大模型的 UI 自动化系统

基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…

USB扩展器与USB服务器的2个主要区别

在现代办公和IT环境中&#xff0c;连接和管理USB设备是常见需求。USB扩展器&#xff08;常称USB集线器&#xff09;与USB服务器&#xff08;如朝天椒USB服务器&#xff09;是两类功能定位截然不同的解决方案。前者主要解决物理接口数量不足的“近身”连接扩展问题&#xff0c;而…

Nuxt.js 中的路由配置详解

Nuxt.js 通过其内置的路由系统简化了应用的路由配置&#xff0c;使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…

验证负载均衡与弹性伸缩

什么是弹性伸缩&#xff08;Auto Scaling&#xff09;&#xff1f; 弹性伸缩是指 云计算平台根据实时负载自动调整计算资源&#xff08;如服务器实例、容器Pod&#xff09;数量&#xff0c;以确保系统在高峰时保持稳定&#xff0c;在低谷时节省成本。 什么时候会触发弹性伸缩&…

区分viewmodel和model职责的方法

gpt回答挺好的&#xff0c;我就分享一下。 1. 最经典的一句话区分 Model&#xff08;Repository/数据层&#xff09;&#xff1a;只负责**“数据获取/存储/持久化”和“核心业务算法”**&#xff0c;不依赖UI层和Android框架&#xff0c;可以脱离界面独立存在。 ViewModel&…

C语言数据结构笔记3:Union联合体+结构体取8位Bool量

本文衔接上文要求&#xff0c;新增8位bool量的获取方式。 目录 问题提出&#xff1a; Union联合体struct结构体(方式1)&#xff1a; Union联合体struct结构体(方式2)&#xff1a; BYTE方式读取&#xff1a; 问题提出&#xff1a; 在STM32单片机的编程中&#xff0c;无法定义Boo…

三种读写传统xls格式文件开源库libxls、xlslib、BasicExcel的比较

最近准备读写传统xls格式文件&#xff0c;而不是较新的xlsx&#xff0c;询问DeepSeek有哪些开源库&#xff0c;他给出了如下的简介和建议&#xff0c;还给出了相应链接&#xff0c;不过有的链接已失效。最后还不忘提醒&#xff0c;现在该用xlsx格式了。 以下是几个可以处理传统…