golang 键值对_对Golang地图的一些见解
golang 键值对
文章是关于地图的内部结构,哈希值和性能的。 数据实际上是如何存储在内部的。
基础概述
地图(又名关联数组)基本上是具有真正快速查找的键值存储。 真正的基本例子:
m :=make ( map [ int ] string )
m[ 42 ] = "Hello!"
value, exist := m[ 42 ]
// value = 42
// exist = true
也可以将地图用作集合:
set :=make ( map [ int ] struct {})
_, exist := set[ 42 ]
if exist {fmt.Println( "42 is in the set" )
}
因为大小为零,所以使用struct {}。 在这里查看小样本 https://play.golang.org/p/Ndn8UqxWYC3
更深入
引擎盖下有一个哈希表(又名哈希图)。 有时,搜索树还用于组织其他一些语言和案例的地图。
哈希映射的主要思想是具有接近恒定的O(1)查找时间。 如果哈希函数产生n次冲突,则正式的查找时间为O(n),这在实际情况下几乎是不可能的。
哈希和碰撞
根据定义,哈希是将任何值转换为固定大小值的函数。 如果使用Go,它将占用任意数量的位并将其转换为64。
内部哈希实现可以在这里找到:
https://github.com/golang/go/blob/dev.boringcrypto.go1.13/src/runtime/hash64.go
当哈希函数针对不同的值产生相同的结果时,这就是冲突。 大多数安全哈希函数没有已知的冲突。
简短的哈希图理论
要存储键值对,最幼稚的方法是创建一个列表,并遍历每个具有O(n)复杂度的对。 不好。
更高级的方法是创建平衡的搜索树并实现稳定的O(log(n))查找效率,这在许多情况下都是很好的。
好的,但是如果我们想做得更快呢? 最快的查找速度是O(1),我们使用数组的速度如此快。 但是我们没有适合数组的键。 解决方案是对键进行哈希处理,并使用哈希或哈希的一部分。
让我们创建BN存储桶。 每个存储桶都是一个简单的列表。 哈希函数将像这样转换密钥:hash(key)-> {1..BN}
向哈希表添加新的键值会将其插入到hash(key)存储桶中。
![](/assets/blank.gif)
查询时间
令PN为哈希图中的键值对的数量。
如果我们的PN≤BN且碰撞为零,这是理想情况,查找时间为O(1)。
如果PN> BN,则至少一个存储桶将至少具有2个值,因此查找可能需要2个步骤-找到存储桶,然后简单地遍历存储桶列表,直到找到键值对。
桶中平均项目数称为负载率。 负载因子除以2将是平均查找时间,而冲突率将定义其分散度。 形式上,复杂度仍然是O(n),但实际上冲突很少发生,并且密钥几乎均匀地散布到存储桶中。
Hashmap Go实施
地图的源代码在这里,或多或少可以理解https://github.com/golang/go/blob/master/src/runtime/map.go
Maps是指向hmap结构的指针。 这是可变的! 如果需要新的副本,则应手动创建副本。
另一个重要的事情是,散列基于随机种子,因此每个映射的构建都不同。
h.hash0 = fastrand()
我认为,这种随机化背后的思想是避免恶意行为者将哈希映射充满冲突时避免潜在的漏洞,并防止任何依赖于映射迭代顺序的尝试。 地图是可迭代的,但不是有序的。 甚至同一映射的每个新迭代器都是随机的。
m :=make ( map [ int ] int )
for j := 0 ; j < 10 ; j++ {m[j] = j * 2
}for k, v := range m {fmt.Printf( "%d*2=%d " , k, v)
}fmt.Printf( "\n - - -\n" )for k, v := range m {fmt.Printf( "%d*2=%d " , k, v)
}
// Second loop of the same map would produce different order of elements
此代码将以随机顺序打印结果。 在此处查看更多代码 https://play.golang.org/p/OzEhs4nr_30
默认地图大小为1个存储桶,存储桶大小为8个元素。 IRL哈希函数通常返回32、64、256…位。 因此,我们仅将这些位的一部分用作存储区编号。 因此,对于8个存储桶,我们仅需要log2(8)= 3个低阶位,(8 =2³,对于16 log2(16)= 4,16 =2⁴,依此类推。
bucket := hash & bucketMask(h.B)
其中hB是BN的log_2。 与2 ^ hB = BN相同
bucketMask(hB)是一个简单的hB位掩码,如00…011…1,其中1位数是hB
地图成长
如果地图超出了特定限制,Go将使存储桶数量增加一倍。 每次增加存储分区的数量时,都需要重新哈希所有地图内容。 在引擎盖下,Go使用复杂的地图增长机制来实现最佳性能。 在一段时间内,Go会将旧存储桶与新存储桶保持在一起,以避免负载高峰,并确保已经启动的迭代器可以安全完成。
地图的条件源于源代码:
overLoadFactor(h.count+1 , h.B) || tooManyOverflowBuckets(h.noverflow, h.B)
首先是负载系数检查。 触发增长的存储桶的平均负载为6.5或更高。 该值是硬编码的,并且基于Go团队基准测试。
第二次检查是关于溢出桶计数。 “太多”是指(大约)溢出桶与常规桶一样多。 单桶硬编码容量为8个元素。 达到限制后,新的溢出桶将链接到满桶。
有趣的是,对于具有≥2¹⁶的水桶的大型地图,该增长触发器是随机的。
func (h *hmap) incrnoverflow () {// We trigger same-size map growth if there are// as many overflow buckets as buckets.// We need to be able to count to 1<<h.B.if h.B < 16 {h.noverflow++return}// Increment with probability 1/(1<<(h.B-15)).// When we reach 1<<15 - 1, we will have approximately// as many overflow buckets as buckets.mask := uint32 ( 1 )<<(h.B -15 ) - 1// Example: if h.B == 18, then mask == 7,// and fastrand & 7 == 0 with probability 1/8.if fastrand()&mask == 0 {h.noverflow++}
}
用不安全的方法破解地图
要收集更多信息,我们需要访问地图内部值,例如hB和存储桶内容。 仅当hmap的内部结构不会更改时,这才可以正常工作(已针对1.13.8测试)
我们需要将内置的地图转换为hmap结构,以便收集地图内部数据。 诀窍是使用unsafe.Pointer和空接口。 首先,我们将映射指针&m转换为unsafe.Pointer ,然后转换为emptyInterface结构,此结构与实际的空接口内部结构匹配。 从该结构中,我们可以获取map as和hmap结构的类型。
type emptyInterface struct {_type unsafe.Pointervalue unsafe.Pointer
}func mapTypeAndValue (m interface {}) (*maptype, *hmap) {ei := (*emptyInterface)(unsafe.Pointer(&m))return (*maptype)(ei._type), (*hmap)(ei.value)
}
并像这样使用它:
m :=make ( map [ int ] int )
t, hm := mapTypeAndValue(m)
我们需要复制maptype, hmap一些 来自Go来源src / runtime / map.go的常量,结构和函数,来源版本 应该匹配编译器版本。
现在,我们可以跟踪地图结构存储区数量如何随着添加新元素而变化。
m :=make ( map [ int ] int )
_, hm := mapTypeAndValue(m)fmt.Printf( "Elements | h.B | Buckets\n\n" )var prevB uint8
for i := 0 ; i < 100000000 ; i++ {m[i] = i * 3if hm.B != prevB {fmt.Printf( "%8d | %3d | %8d\n" , hm.count, hm.B, 1 <<hm.B)prevB = hm.B}
}
代码段-https: //play.golang.org/p/NaoC8fkmy9x
Elements | h.B | Buckets9 | 1 | 214 | 2 | 427 | 3 | 853 | 4 | 16105 | 5 | 32209 | 6 | 64417 | 7 | 128833 | 8 | 2561665 | 9 | 5123329 | 10 | 10246657 | 11 | 204813313 | 12 | 409626625 | 13 | 819253249 | 14 | 16384106497 | 15 | 32768212993 | 16 | 65536425985 | 17 | 131072851969 | 18 | 2621441703937 | 19 | 5242883407873 | 20 | 10485766815745 | 21 | 2097152
13631489 | 22 | 4194304
27262977 | 23 | 8388608
54525953 | 24 | 16777216
现在,让我们看看如何填充水桶! 为了相对简单,我将忽略溢出存储桶的内容。 Hmap结构包含指向存储在内存中单元格的指针桶 。 接下来,我们遍历低谷内存以获取地图存储桶中的所有值。 在这里我们需要密钥大小和地图类型 bucketsize到 计算存储桶和单元格的正确偏移量。
func showSpread (m interface {}) {// dataOffset is where the cell data begins in a bmapconst dataOffset = unsafe.Offsetof( struct {tophash [bucketCnt] uint8cells int64}{}.cells)t, h := mapTypeAndValue(m)fmt.Printf( "Overflow buckets: %d" , h.noverflow)numBuckets := 1 << h.Bfor r := 0 ; r < numBuckets*bucketCnt; r++ {bucketIndex := r / bucketCntcellIndex := r % bucketCntif cellIndex == 0 {fmt.Printf( "\nBucket %3d:" , bucketIndex)}// lookup cellb := (*bmap)(add(h.buckets, uintptr (bucketIndex)* uintptr (t.bucketsize)))if b.tophash[cellIndex] == 0 {// cell is emptycontinue}k := add(unsafe.Pointer(b), dataOffset+ uintptr (cellIndex)* uintptr (t.keysize))ei := emptyInterface{_type: unsafe.Pointer(t.key),value: k,}key := *(* interface {})(unsafe.Pointer(&ei))fmt.Printf( " %3d" , key.( int ))}fmt.Printf( "\n\n" )
}func main () {m := make ( map [ int ] int )for i := 0 ; i < 50 ; i++ {m[i] = i * 3}showSpread(m)m = make ( map [ int ] int )for i := 0 ; i < 8 ; i++ {m[i] = i * 3}showSpread(m)
}
由于地图哈希生成的随机性,大于8个值的地图的每个运行结果都会有所不同。 注意,非常小的地图将像列表一样工作! 结果示例:
Overflow buckets: 3
Bucket 0: 26 28
Bucket 1: 0 2 7 8 13 22 31 38
Bucket 2: 6 9 11 16 21 34 35 37
Bucket 3: 1 4 12 14 29 42 48
Bucket 4: 10 32 45 46
Bucket 5: 15 30 33 43
Bucket 6: 25 47
Bucket 7: 3 5 17 18 19 20 23 24Overflow buckets: 0
Bucket 0: 0 1 2 3 4 5 6 7
程式码片段 https://play.golang.org/p/xgyQEatPHgT
预定义尺寸
有时您现在需要在地图中放入多少个项目。 对于已知大小的地图,最好在创建时指定大小。 Go将自动创建合适数量的存储桶,因此可以避免增长过程的费用。
m :=make ( map [ int ] int , 1000000 )
_, hm := mapTypeAndValue(m)fmt.Printf( "Elements | h.B | Buckets\n\n" )fmt.Printf( "%8d | %3d | %8d\n" , hm.count, hm.B, 1 <<hm.B)for i := 0 ; i < 1000000 ; i++ {m[i] = i * 3
}fmt.Printf( "%8d | %3d | %8d\n" , hm.count, hm.B, 1 <<hm.B)
结果:
Elements | h.B | Buckets0 | 18 | 2621441000000 | 18 | 262144
程式码片段 https://play.golang.org/p/cnijjiKwM8o
删除和不断增长的地图
您应该了解Go内置地图,因为它们只能增长 。 即使您从映射中删除所有值,存储桶数也将保持不变,内存消耗也将保持不变。
m :=make ( map [ int ] int )
_, hm := mapTypeAndValue(m)fmt.Printf( "Elements | h.B | Buckets\n\n" )for i := 0 ; i < 100000 ; i++ {m[i] = i * 3
}for i := 0 ; i < 100000 ; i++ {delete (m, i)
}fmt.Printf( "%8d | %3d | %8d\n" , hm.count, hm.B, 1 <<hm.B)
结果:
Elements | h.B | Buckets0 | 14 | 16384
程式码片段 https://play.golang.org/p/SPWixru8sdM
链接和感谢
文章中提到的所有代码都可以在这里找到: https : //github.com/kochetkov-av/golang-map-insights
非常感谢https://lukechampine.com/hackmap.html的作者,当然还要感谢The Go Authors!
翻译自: https://hackernoon.com/some-insights-on-maps-in-golang-rm5v3ywh
golang 键值对
golang 键值对_对Golang地图的一些见解相关推荐
- list python 访问 键值对_基础|Python常用知识点汇总(中)
字符串字符串是 Python 中最常用的数据类型.我们可以使用引号('或")来创建字符串.1.创建字符串 str1 = 'Hello World!' str2 = "Hello W ...
- list python 访问 键值对_学完Python,我决定熬夜整理这篇总结...
作者:Caso_卡索 来源:http://suo.im/5wzRqt 一.了解Python 1.Python之父 Guido Van Rossum,一位荷兰程序员,在1989年圣诞节编写了Pyhon语 ...
- php数组去掉键值,PHP 如何将数组去掉键值?_后端开发
如何用python计算圆周率?_后端开发 python计算圆周率的方法:首先在图像中随机抛置大量的点:然后计算落在1/4圆内的点的数量:最后计算pi值,代码为[for i in range(1,DAR ...
- php 生成动态键值 数组_你的PHP项目遇到性能问题了吗?看完这篇性能分析恍然大悟...
你的项目中遇到性能问题了吗?遇到性能问题你是如何解决的呢?你的解决方式是否正确呢?下面就跟大家一起分享php项目的性能问题. PHP语言级性能分析 php在什么情况下会遇到性能问题呢? 在讨论性能问题 ...
- vue 往对象中添加键值对_【Vue】Vue学习之混入
今天学习了Vue中的"混入"知识点,写篇文章用自己的语言来向自己解释它,如有不足还望指点. 混入(mixins): 混入提供了一种非常灵活的方式,来分发Vue组件中的可复用功能 - ...
- python3 循环写入一对多键值对_为什么Python 3.6以后字典有序并且效率更高?
在Python 3.5(含)以前,字典是不能保证顺序的,键值对A先插入字典,键值对B后插入字典,但是当你打印字典的Keys列表时,你会发现B可能在A的前面. 但是从Python 3.6开始,字典是变成 ...
- python 字典键值重复_浅谈python字典多键值及重复键值的使用
在python中使用字典,格式如下: dict={ key1:value1 , key2;value2 ...} 在实际访问字典值时的使用格式如下: dict[key] 多键值 字典的多键值形式如下: ...
- 如何动态的向数组中插入键值对_在Java中实现的一个简单“HashMap”
如何创建Hash表 对于把K(键)-V(值)这样的键值对插入Hash表中,需要执行两个步骤: 1.使用散列函数将K转换为小整数(称为其哈希码). 2.哈希码用于查找索引(hashCode%arrSiz ...
- python中字典按键或键值排序_[宜配屋]听图阁
字典排序 在程序中使用字典进行数据信息统计时,由于字典是无序的所以打印字典时内容也是无序的.因此,为了使统计得到的结果更方便查看需要进行排序.Python中字典的排序分为按"键"排 ...
最新文章
- 3.放弃CHAR吧,在铸成大错之前!
- 神策 2021 数据驱动大会,与“现代营销之父”科特勒的认知同行
- linux之pmap命令
- Mint-ui框架Index List 的应用,以及高度的适配问题
- jenkins自动化构建iOS应用配置过程中遇到的问题
- VTP与三层交换配置实验
- 2.Jenkins 2 权威指南 --- 基础知识
- 转:Git: 对象原理
- 斐讯K2从第三方固件刷回原厂固件
- 解读BOLT引擎例子——HelloBolt2
- 量化投资学习——股票数据接口的汇总和整理
- java 多用组合_java 为什么说多用组合,少用继承?
- 自然语言处理(二)基于CNN的新闻文本分类
- js 拖动图片移动位置插件jquery.dad.js
- linux wenj 立即生效_Linux系统调用(转载)
- DLMS/COSEM (IEC 62056, EN13757-1)协议简介
- 小桥 流水 房子 水闸
- aspen模拟蒸发器_用Aspen Plus设计蒸发器例题
- bbz10 android,10万应用20%靠安卓移植 黑莓BB10原生难
- NR2_unpacker与TIDTool使用例--提取游戏《超次元海王星 重生2》(Hyperdimension Neptunia ReBirth2)中的文件(文件后缀为.pac和.tid的解包方法)