Go之map详解

map的结构

map实现的两个关键数据结构

  • hmap 定义了map的结构
  • bmap 定义了hmap.buckets中每个bucket的结构
// A header for a Go map.
type hmap struct {count     int // 元素的个数flags     uint8 // 状态标记,标记map当前状态,是否正在写入B         uint8   // 可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子noverflow uint16 // 溢出的个数hash0     uint32 // 哈希种子buckets    unsafe.Pointer // 桶的地址oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容nevacuate  uintptr        // 迁移进度,小于nevacuate的已经迁移完成extra *mapextra // optional fields
}
// A bucket for a Go map.
type bmap struct {//每个元素hash值的高8位,如果tophash[0] < minTopHash,表示这个桶的搬迁状态tophash [bucketCnt]uint8// 接下来是8个key、8个value,但是我们不能直接看到;为了优化对齐,go采用了key放在一起,value放在一起的存储方式,// 再接下来是hash冲突发生时,下一个溢出桶的地址
}
//上面bmap结构是静态结构,在编译过程中runtime.bmap会拓展成以下结构体:
type bmap struct{tophash [8]uint8keys [8]keytype// keytype由编译器编译时候确定values [8]elemtype// elemtype由编译器编译时候确定overflow uintptr//overflow的下一个bmap,overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}
名词解释
负载因子

负载因子是衡量hash表中当前空间占用率的指标。在go中,就是平均每个bucket存储的元素个数。

  • 计算公式如下:

LoadFactor(负载因子)= hash表中已存储的键值对的总数量/hash桶的个数(即hmap结构中buckets数组的个数)在各语言的实现中,都会确定一个负载因子的阈值,当负载因子超过这个阈值时,就要进行扩容,以平衡存储空间和查找元素时的性能。根据golang团队的测试数据,将负载因子定为了6.5,即平均每个bucket保存的元素≥6.5个时会触发扩容。

B

bucket个数为:2^B; 可以保存的元素数量是 负载因子 * 2^B。

data := make(map[int]int,17)

根据计算公式

初始元素个数 ≤ 2^B * 6.5
172^2 * 6.5

可以计算出B为2,初始的桶的个数为4
其中:
B<4时,根据B创建桶的个数的规则为:2^B(标准桶)
B>=4时,根据B创建桶的个数的规则为:2^B + 2^(B-4) (标准桶+溢出桶)

tophash

tophash是一个长度为8的数组,它不仅仅用来存放key的哈希高8位,在不同场景下它还可以标记迁移状态,bucket是否为空等。弄懂tophash对我们深入了解map实现非常重要。

当tophash对应的K/V被使用时,存的是key的哈希值的高8位;当tophash对应的K/V未被使用时,存的是K/V对应位置的状态。

emptyRest      = 0 
emptyOne       = 1 
evacuatedX     = 2 
evacuatedY     = 3
evacuatedEmpty = 4
minTopHash     = 5 

当tophash[i] < 5时,表示存的是状态;
当tophash[i] >= 5时,表示存的是哈希值;

当计算的哈希值小于minTopHash时,会直接在原有哈希值基础上加上minTopHash,确保哈希值一定大于minTopHash。

func tophash(hash uintptr) uint8 {top := uint8(hash >> (sys.PtrSize*8 - 8))if top < minTopHash {top += minTopHash}return top
}
emptyRest

这个值有两层意思:一是表示该tophash对应的K/V位置是可用的;二是表示该位置后面的K/V位置都是可用的。

赋值:

初始化的时,tophash会被置为emptyRest。

删除map元素时,会判断是否需要把删除key对应的tophash置为emptyRest。

作用:

判断bucket是否为空

当tophash[0]==emptyRest表示整个bucket都是空的,这就是源码里面判断bucket是否为空的方法。

查找时快速判断后面位置是否还需遍历

如在查找时,在一个bucket中,找到tophash[2]位置,发现值为emptyRest,就可以判断该bucket没有该元素,继续查找下一个bucket。

emptyOne

仅表示该tophash对应的K/V位置是可用的,其后面的是否可用不知道。

赋值:

删除map元素时,会把key对应的tophash先置为emptyOne,再继续判断是否需要置为emptyRest。

evacuatedX && evacuatedY

这两个状态与扩容有关,记录元素被迁移到了新桶的部位–X或Y。如果是等位迁移,旧桶的元素必然被迁移到X部;如果是扩容迁移,旧桶元素可能迁移到X部,也可能迁移到Y部。当迁移到X部时,旧桶tophash置为evacuatedX;当迁移到Y部时,旧桶tophash置为evacuatedY。

举个例子说明:扩容迁移,要把旧桶1的元素迁到新桶,因为新桶长度增长了一倍,因此旧桶1元素可能被迁移到新桶的1或5。当元素迁移到了1时,把旧桶tophash置为evacuatedX;反之,迁移到了5时,tophash置为evacuatedY。要注意置的是旧桶的tophash。

旧桶迁移完就被回收了,为啥还要记录每个元素迁移的位置?

想了解原因,我们必须要考虑一个很复杂的场景:遍历map时,开始扩容。map遍历并不是原子操作,在遍历过程中会有数据插入、删除等,会导致map扩容。因为遍历发生在扩容前,因此一直是遍历老桶。这时有两种情况:老桶数据没有迁移,这时直接从老桶返回K/V就可以了;老桶数据已经迁移,这时就需要重新查找map。那怎么判断老桶数据是否迁移?这时就用到evacuatedX和evacuatedY,这两个就是用来标记老桶数据迁移状态的。

evacuatedEmpty

用于表示此单元已迁移

创建map

在这里插入图片描述

func makemap_small() *hmap
func makemap(t *maptype, hint int, h *hmap) *hmap
func makemap64(t *maptype, hint int64, h *hmap) *hmap // hint类型为int64, 实质还是调用的 makemap

当创建map时不指定hint大小,那么调用makemap_small来进行创建 当指定了hint(代表初始化时可以保存的元素的个数)的大小的时候,若hint<=8, 使用makemap_small进行创建map,否则使用makemap创建map

map查找

Go map会在编译阶段转换成runtime包中的hmap。其中,bmap指向存储key-value的结构(数组)。数组元素为“桶”,每个桶中包含高8位的hash和相应的8个key-value,高8位hash用来快速找到目标key,其次是8个key,8个value(key和value分开存储,是为了防止key存储空间大于value时,value会自动占用key大小的空间,这样做可以减少空间的浪费),最后是指向溢出桶的指针(解决哈希冲突)。hash 表通过 hash 值的低几位(原理是对数组长度取余,但通常采用与运算来加速)去查找 hash 桶,然后在去查找到的 hash 桶中查找高8位的hash,快速锁定key,知道key,就可以获取其value了。如果遇到哈希冲突,即不同key产生的hash值一样,如此就需要额外进行key值的比较,这就要求存储的key值是可以比较相等的,
在这里插入图片描述
Go map扩容,数据迁移不是一次性迁移,而是等到访问到具体某个bucket时才将数据从旧bucket中迁移到新bucket中

  1. 一次性迁移会涉及到cpu资源和内存资源的占用,在数据量较大时,会有较大的延时,影响正常业务逻辑。因此Go采用渐进式的数据迁移,每次最多迁移两个bucket的数据到新的buckets中(一个是当前访问key所在的bucket,然后再多迁移一个bucket)
  2. cpu资源:扩容时需要迁移map中oldbuckets的元素,其中的rehash操作会消耗cpu的计算资源,有可能会影响到用户协程的调度
map插入/删除

在这里插入图片描述
在这里插入图片描述

扩容的条件:

1. 超过负载 map元素个数>6.5*桶个数
func overLoadFactor(count int, B uint8) bool{return count > bucketCnt && uintptr(count)>loadFactor*bucketShift(B)
}
其中
bucketCnt=8,一个桶可以装的最大元素个数
loadFactor=6.5,负载因子,平均每个桶的元素个数
bucketShift(8), 桶的个数
2. 溢出桶太多
当桶总数<2^15时,如果溢出桶总数>=桶总数,则认为溢出桶过多
当桶总数>=2^15时,直接与2^15比较,当溢出桶总数>=2^15时,即认为溢出桶太多了

扩容机制:

1.双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧的buckets数据
搬迁到新的buckets
2.等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁操作,
把松散的键值对重新排列一次,使得同一个bucket中的key排列地更紧密,节省空间,提高buckets利用
率,进而保证更快的存取。该方法我们称之为等量扩容。

扩容过程:
在这里插入图片描述

假设旧桶数组大小为2^B, 新桶数组大小为2*2^B,对于某个hash值X
若 X & (2^B) == 0,说明 X < 2^B,那么它将落入与旧桶集合相同的索引xi中;
否则,它将落入xi + 2^B中

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

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

相关文章

css层叠性,继承性,优先级

前言 本文概要&#xff1a;讲述css的三大特性&#xff0c;层叠&#xff0c;继承和优先级。 层叠性 描述&#xff1a;我们试想以下这种情况&#xff1a;我们定义了同一个选择器&#xff0c;但是定义的属性不同。属性有相同的也有不同的&#xff0c;那么最后我们这个页面会听谁的…

CSS display属性

目录 概述&#xff1a; 设置display示例&#xff1a; none&#xff1a; block&#xff1a; inline&#xff1a; inline-block &#xff1a; 概述&#xff1a; 在CSS中我们可以使用display属性来控制元素的布局&#xff0c;我们可以通过display来设置元素的类型。 在不设置…

封装个js分页插件

// 分页插件类 class PaginationPlugin {constructor(fetchDataURL, options {}) {this.fetchDataURL fetchDataURL;this.options {containerId: options.containerId || paginationContainer,dataSizeAttr: options.dataSizeAttr || toatalsize, // 修改为实际API返回的数据…

ppt技巧:​如何将两个PPT幻灯片文件合并成一个?

第一种方式&#xff1a;复制粘贴幻灯片 1. 打开第一个PPT幻灯片文件&#xff0c;确保你已经熟悉该文件的内容和布局。 2. 打开第二个PPT幻灯片文件&#xff0c;浏览其中的所有幻灯片&#xff0c;选择你想要合并到第一个文件中的幻灯片。 3. 使用快捷键CtrlC&#xff08;Wind…

虚拟ip地址怎么弄到手机上

在当下的社会&#xff0c;手机已经变得至关重要&#xff0c;它融入了我们的日常生活&#xff0c;无论是上网冲浪、社交互动&#xff0c;还是工作学习&#xff0c;都离不开它。但有时候&#xff0c;由于某些限制&#xff0c;我们可能无法充分享受网络带来的便利。这时&#xff0…

Nginx part2.1

目录 搭建目录网页 为网页设置用户登录 做一个文件目录网页&#xff0c;并进行登陆 示范 搭建目录网页 启动nginx&#xff1a; systemctl start nginx 开机自启动nginx&#xff1a; systemctl enable nginx 启动完服务后&#xff0c;查看自己的nginx的状态&#xff1a;sys…

【JavaWeb】Day47.Mybatis基础操作——删除

Mybatis基础操作 需求 准备数据库表 emp 创建一个新的springboot工程&#xff0c;选择引入对应的起步依赖&#xff08;mybatis、mysql驱动、lombok&#xff09; application.properties中引入数据库连接信息 创建对应的实体类 Emp&#xff08;实体类属性采用驼峰命名&#xf…

【C++提高】常用容器

常用容器 引言&#xff1a;迭代器的使用一、vector容器1. vector基本概念2. vector的迭代器3. vector构造函数4. vector赋值操作5. vector容量和大小6. vector插入和删除7. vector数据存取8. vector互换容器9. vector预留空间 二、deque容器1. deque容器的基本概念2. deque容器…

python免费调用阿里云通义千问(q-wen-max)大模型API

文章目录 通义千问开通免费API Keypython调用阿里云通义千问API 通义千问 通义千问&#xff0c;是基于阿里巴巴达摩院在自然语言处理领域的研究和积累。采用更先进的算法和更优化的模型结构&#xff0c;能够更准确地理解和生成自然语言、代码、表格等文本。 支持更多定制化需…

HarmonyOs开发:导航tabs组件封装与使用

前言 主页的底部导航以及页面顶部的切换导航&#xff0c;无论哪个系统&#xff0c;哪个App&#xff0c;都是最常见的功能之一&#xff0c;虽然说在鸿蒙中有现成的组件tabs可以很快速的实现&#xff0c;但是在使用的时候&#xff0c;依然有几个潜在的问题存在&#xff0c;第一&a…

GRAF: Generative Radiance Fields for 3D-Aware Image Synthesis

GRAF: Generative Radiance Fieldsfor 3D-Aware Image Synthesis&#xff08;基于产生辐射场的三维图像合成&#xff09; 思维导图&#xff1a;https://blog.csdn.net/weixin_53765004/article/details/137944206?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3…

THREEJS 使用CatmullRomCurve3实现汽车模型沿着指定轨迹移动

效果预览 准备所需资源 搭建场景环境 const container document.querySelector("#box_bim");// 创建摄像机camera new THREE.PerspectiveCamera(50,window.innerWidth / window.innerHeight,0.1,1000);// camera.position.set(500, 500, 500);// 调整近裁减值camer…

深入剖析图像平滑与噪声滤波

噪声 在数字图像处理中&#xff0c;噪声是指在图像中引入的不希望的随机或无意义的信号。它是由于图像采集、传输、存储或处理过程中的各种因素引起的。 噪声会导致图像质量下降&#xff0c;使图像失真或降低细节的清晰度。它通常表现为图像中随机分布的亮度或颜色变化&#…

面试不慌张:一文读懂FactoryBean的实现原理

大家好&#xff0c;我是石头~ 在深入探讨Spring框架内部机制时&#xff0c;FactoryBean无疑是一个关键角色&#xff0c;也是面试中经常出现的熟悉面孔。 不同于普通Java Bean&#xff0c;FactoryBean是一种特殊的Bean类型&#xff0c;它的存在并非为了提供业务逻辑&#xff0c;…

基于Springboot的小区物业管理系统

基于SpringbootVue的小区物业管理系统的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 用户管理 员工管理 业主信息管理 费用信息管理 楼房信息管理 保修信息…

【原创教程】海为PLC与RS-WS-ETH-6传感器的MUDBUS_TCP通讯

一、关于RS-WS-ETH-6传感器的准备工作 要完成MODBUS_TCP通讯,我们必须要知道设备的IP地址如何分配,只有PLC和设备的IP在同一网段上,才能建立通讯。然后还要选择TCP的工作模式,来建立设备端和PC端的端口号。接下来了解设备的报文格式,方便之后发送报文完成数据交互。 1、…

自定义Blazor单文件Web程序端口

#接 上篇 Mysql快速迁移版的制作过程# 上一篇《Mysql8快速迁移版的制作过程》完成了快速迁移的数据库的准备&#xff0c;今天接着讲基于Blazor的Web程序快速迁移版的制作。 单文件发布的难点不在发布而是因为程序系统默认给了个5001的端口&#xff0c;而是如何能够让用户自定…

AOP基础-动态代理

文章目录 1.动态代理1.需求分析2.动态代理的核心3.代码实例1.Vehicle.java2.Car.java3.Ship.java4.VehicleProxyProvider.java(动态代理模板)5.测试使用 2.动态代理深入—横切关注点1.需求分析2.四个横切关注点3.代码实例1.Cal.java2.CalImpl.java3.VehicleProxyProvider02.jav…

0-1背包问题:贪心算法与动态规划的比较

0-1背包问题&#xff1a;贪心算法与动态规划的比较 1. 问题描述2. 贪心算法2.1 贪心策略2.2 伪代码 3. 动态规划3.1 动态规划策略3.2 伪代码 4. C语言实现5. 算法分析6. 结论7. 参考文献 1. 问题描述 0-1背包问题是组合优化中的一个经典问题。假设有一个小偷在抢劫时发现了n个…

(C语言)sscanf 与 sprintf详解

目录 1.sprintf函数详解 2. sscanf函数详解 1.sprintf函数详解 头文件&#xff1a;stdio.h 作用&#xff1a;将格式化的数据写入字符串里&#xff0c;也就是将格式化的数据转变为字符串。 演示&#xff1a; #include <stdio.h> struct S {char name[10];int height;…