首页 > 其他分享 >golang map

golang map

时间:2023-11-16 14:36:54浏览次数:32  
标签:扩容 map bucket hmap golang key go

golang 的 map 使用的是 hash map

基本结构

下面截取自源码,已翻译

// runtime/map.go:117

// go map 定义,hashmap 缩写
type hmap struct {
	count     int // map 里文件数
	flags     uint8 // map 当前是否在写入,一般为 hashWriting  = 4 (写入中)或 0 (空闲)
	B         uint8  // 桶的数量,2^B 个
	noverflow uint16 // 溢出桶的数量
	hash0     uint32 // hash 随机数,从 hmap 创建开始就不变

	buckets    unsafe.Pointer // 存储桶的指针,存放 2^B 个桶的地址
	oldbuckets unsafe.Pointer // 旧存储桶的地址,在扩容时候用
	nevacuate  uintptr        // 记录疏散桶进度 TODO

	extra *mapextra // 当 bucket 不含指针时,记录所有溢出桶的地址,加快 gc
}

// runtime/map.go:151

// 一个桶的设计
type bmap struct {
	tophash [bucketCnt]uint8 // 高 64 位存储每个 key 的信息
	// 后面跟 8 个 key
	// 然后跟 8 个 value
	// 下一个溢出桶地址
}

初始化

m  := make(map[int]string) // 初始化一个empty的 map,所有参数都是 0,用的方法是h := new(hmap) 参考runtime/map.go:294
m2 := make(map[int]string,100) // 初始化一个大小为 128 的 map
m3 := map[int]string{1:"a"} // 初始化 1 个桶的 map

我的golang是 1.21 版本(go version go1.21.1 darwin/arm64)
先分析使用 make 的情况,当 make 的第二个参数不填或者<=8的时候,调用的是 makemap_small(runtime/map.go:294) 函数,但我本地看汇编代码并没有发现 makemap_small 函数,应该是编译器有别的优化,具体可以深入研究一下,其他版本的 golang 还没测。
这里其实进行了很简单的内存分配和随机种子,所以找不到也可以理解

// runtime/map.go:294

func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand() // 随机 hash 种子
	return h
}

当第二个参数 >8的时候,会调用 makemap(runtime/map.go:305) 函数,这个从汇编代码可以看到,会进行 bucket 的内存分配

// runtime/map.go:305

func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// 这块跟 makemap_small是一样的
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()

	// 这块主要是计算桶的数量
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// 这块进行 bucket 的初始化
	if h.B != 0 {
		var nextOverflow *bmap
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

同时在 key 是 32 位,64 位,string 时,都用的同一样的创建 map,但是插入、读取、删除函数有各自的优化,否则就用通用的插入读取删除

插入

先对 key 进行 hash(hash 函数运行时得到,根据处理器有关,一般是 aes),得到 64 位的哈希值
用哈希值的低 B (hmap.B) 位来确定该 key 落入哪个桶内
再用哈希值的高 8 位寻找在 bucket 里的位置
然后依次找到空的位置,将 key,value 写入 bucekt 里的对应位置
如果当前 bucket 满了,会触发溢出桶,新建一个 bucket 的操作

读取

读取和插入类似,先对 key 进行 hash 运算得到 64 位 hash 值
然后依次计算低 B 位,高 8 位
然后找到对应的桶,再依次找高八位相同的值,再比较 key,所以能作为 map 的 key 的一定是可比较的类型,也就是支持==操作
如果找不到返回默认值

扩容

map 在元素增加过多或过于稀疏都会发生扩容

  1. 当装载因子大于 6.5,会发生扩容
  2. 当溢出桶的数量过多,但是装载因子却 < 6.5,说明map 比较稀疏,就需要sameSizeGrow,称作等量扩容

扩容

扩容就是增加一倍的 bucket 数量,把原来的某个 bucket 的元素重新取低B位,然后放到新的桶里

等量扩容

等量扩容也是走的扩容流程,只不过B 不+1,只是新建一个 bucket,将原来 bucket 里的数据搬迁到新的 bucket 里,当有多个溢出桶时候可能会压缩成一个,就没有溢出桶了

遍历

首先明确,map 在 range 遍历的时候返回的是值的拷贝,而不是原值,所以对遍历的值的修改对原值不会影响,如果遍历的 value 是指针的话,就相当于拿指针修改,就会有影响,但是 map 里存的值不会变

map 在遍历的时候先随机一个种子,然后从一个随机的 bucket 和随机的位置开始遍历
如果在扩容中,会查看 bucket 是否正在扩容,如果是正在扩容,回去老的 bucket 就是 oldbuckets 那遍历

难点

扩容

map 的主要难点就是扩容相关

并发

map 在写入时会将 flag 置为hashWriting,其他协程有写操作时候,会 panic

参考

深入解析Golang的map设计
逐行拆解 Go map 源码
Go语言基础结构 —— Map(哈希表)
golang map 从源码分析实现原理(go 1.14)
我可能并不会使用golang map
GO语言设计与实现-3.3哈希表
Go 程序员面试笔试宝典-哈希表

标签:扩容,map,bucket,hmap,golang,key,go
From: https://www.cnblogs.com/elve960520/p/17836145.html

相关文章

  • Golang把文件写到excel
    最近有个需求是把看广告的日志转成excelpackagemainimport( "bufio" "encoding/json" "flag" "fmt" "github.com/xuri/excelize/v2" "os" "time")//Ad广告typeAdstruct{ OpenIdstring`json:&quo......
  • 使用golang对服务器简单监控
    packagemainimport( "fmt" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/host" "github.com/shirou/gopsutil/load" "github.com/shirou/gopsutil/me......
  • HashMap集合的map.values()返回的Collection集合执行add方法报空指针问题
    一、方法1、privateCollection<String>setPermissionTenant(List<SysPermission>ls,inttenantId){//循环两次第一次设置ID和tenantId第二次设置pidMap<String,String>map=newHashMap<>();for(SysPermissionp:ls){......
  • 从一道题来看看golang中的slice作为参数时的现象
    1、题目最近看群友在群里问一道关于golang中slice的题,题目如下:packagemainimport"fmt"funcmain(){ k:=[]int{1,2,3,4} k=append(k,5,6) fmt.Printf("k-->value:%v,add:%p,cap:%d\n",k,k,cap(k)) ap(k) fmt.Printf("k-->value......
  • 记hashmap
    hashmap是map接口的一个实现类,在同步的情况下hashmap的性能是比较好的 hashmap就是一个kv键值对的集合,将数值散列均匀的存储在哈希表中。插入方法为map.put(k,v),读取方法为map.get(k,v)允许使用null键和null值,会被默认为0hashmap采用的是数组+链表的存储方式,当链表长度>8时会......
  • React.Children.map的用法
    React.Children用很多用法,如下图,经常会用到的是toArray(),具体用法可以自行了解,这里记录下map()的用法和使用到的场景。1.用法:React.Children.map接收2个参数,第一个是所有子元素,第二个是个回调,可以对每个子元素进行处理,然后返回处理后的子元素。2.使用场景:子元素(也可理解为......
  • windows系统使用终端和goland编辑器打包golang程序方法
    上一篇文章说了,windows系统,如何使用goland编辑器打包exe和linux程序,这篇文章再补充一下,使用终端和goland编辑器打包的对比情况。这里的终端可以是,cmd、WindowsPowerShell、MINGw64这里,我使用goland编辑器里面的Terminal,也就是WindowsPowerShelll来操作1、goland编辑器打......
  • 【golang】Golang 哈希码 hashcode 输入一个字符串,得到一个唯一标识码
    如何输入一个字符串,得到一个唯一的hashcode?例子如下:packagemainimport("fmt""hash/crc32")//Stringhashesastringtoauniquehashcode.////crc32returnsauint32,butforouruseweneed//andnonnegativeinteger.Herewec......
  • Map---IdentityHashMap
    概述Thisclassimplementsthe<tt>Map</tt>interfacewithahashtable,usingreference-equalityinplaceofobject-equalitywhencomparingkeys(andvalues).Inotherwords,inan<tt>IdentityHashMap</tt>,twokeys<tt>k1<......
  • 如何在mapbox中将标注添加到面
    consttestGeoJOSN=()=>{//加载GeoJSON数据map.addSource("geojson",{type:"geojson",data:china,generateId:true,});map.addLayer({id:"china",type:"fill",......