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)存储桶中。
查询时间
令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!
From: https://hackernoon.com/some-insights-on-maps-in-golang-rm5v3ywh
Golang地图的一些见解相关推荐
- golang 键值对_对Golang地图的一些见解
golang 键值对 文章是关于地图的内部结构,哈希值和性能的. 数据实际上是如何存储在内部的. 基础概述 地图(又名关联数组)基本上是具有真正快速查找的键值存储. 真正的基本例子: m :=make ...
- 使用Python+Folium实现地理空间可视化效果
概述 如今,有多个数据科学项目需要使用交互式地图.可以通过各种工具制作这种交互式绘图,其中一种工具是 Python 的 Folium 库 本文重点介绍使用 Folium 库创建令人印象深刻的地理可视化 ...
- golang rsa密钥_如何在Golang的地图中检查密钥是否存在?
golang rsa密钥 When you try to get the value of key in a map, you get two return values. The first val ...
- 如何写出优雅的 Golang 代码
Go 语言是一门简单.易学的编程语言,对于有编程背景的工程师来说,学习 Go 语言并写出能够运行的代码并不是一件困难的事情,对于之前有过其他语言经验的开发者来说,写什么语言都像自己学过的语言其实是有问 ...
- 20年吐血整理:程序员全栈体系化学习路线与进阶地图
左耳朵耗子的程序员练级攻略基本上发布完了,全程看下来,不得不说这些文章能看出来,是花了很大的心血,调动了耗子20年软件开发相关工作经验,结合他的亲身经历,分享他一路走来的经验和总结. 比如,如何利用技 ...
- 基于ArcGIS API for JavaScript加载百度各种类型切片地图
文章目录 应用场景 需求分析 效果图 实现代码 原理解读 应用场景 部分项目基于ArcGIS平台,但是甲方只提供部分矢量数据,用作底图的地形图数据没有,表示可以使用百度地图作为底图.所以才会有使用Ar ...
- GIS创新实践【实验1】郑州市地图制作与发布
GIS创新实践[实验1]郑州市地图制作与发布 GIS创新实践[实验2]疫情地图制作与发布 目录 一.目的与任务 二.实验内容与安排方式 三.实验环境 四.实验设计与步骤 01.调出ArcGis软件的编 ...
- tableau地图城市数据_优阅达“优分享” | Tableau 2020.4 “地图标记层” 的多种妙用...
前不久,Tableau 2020.4 发布了!众多新功能特性让数据粉激动不已. 其中,空间分析"地图标记层"功能,被称为 Tableau 开发团队史上最大胆的设计之一.它与 Ado ...
- wms地图绘制工具_移情地图,了解用户需求的利器
如果你想打造一款成功的产品,对你的用户有一个良好的了解是至关重要的.虽然用户体验设计师有许多技能可以帮助他们发展这种理解,但有一种关键技能有很多优势,它称为移情地图. User-Experience ...
最新文章
- 抄代码的时候总是遇到原始数据应该长什么样的问题??
- 华为服务器型号查询,服务器设备型号查询
- 比较器 Comparable 与compartor 的区别及理解
- pfSense book之硬件配置指南
- Linux 支持显卡sli么,AMD Vega20专业卡将支持XGMI总线交火
- 北斗导航 | GNSS卫星导航天线在车载高精度定位领域中的应用与挑战
- MAUI中Maui.Graphics.Controls绘制控件
- 控制元素的div属性
- linux shell取变量的子串26种方法实践
- python打包成二进制文件_pyinstall python文件打包成二进制exe文件
- 苹果抄袭豌豆射手实锤!AirPods Pro又被玩坏了...
- 苹果卖这么贵都怪她?苹果零售部门主管将离职 曾是奢侈品巨头掌门人
- Linux关于DHCP详细的总结
- 5个音效素材网站,赶紧收藏
- 宝马 android手机同步,BMW将发布无线安卓互联系统,CarPlay终于不再一家独大!
- java数组下标从几开始的_为什么数组角标从0开始
- 构建分布式系统——技术考量
- android 卸载内置app,安卓全机型卸载预装软件
- outlook邮箱邮件大小限制_Office Outlook 2010、2013附件大小超过了允许的范围限制三种解决方法图解 – 爱分享...
- Linux下clock_gettime函数详解
热门文章
- 通过用户名密码登陆人人网
- 【Home Assistant 之QQ邮箱推送提醒】
- Windows Phone理解和运用ItemTemplate、ContentTemplate和DataTemplate
- 【微信】测试token用的index.php
- 从PowerBuilder+wiseinstaller程序发布看windows的system32目录共享
- Win11如何查看自己的内网主机ip
- 传智播客JAVA培训2010-4-29Lucene总结
- IT男的真实人生:总被误认为是修电脑的
- “网易云阅读”-移动架构
- 修练8年C++面向对象程序设计之体会(文章不错)