欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Golang地图的一些见解

程序员文章站 2022-04-20 21:48:50
...

文章是关于地图的内部结构,哈希值和性能的。 数据实际上是如何存储在内部的。

基础概述

地图(又名关联数组)基本上是具有真正快速查找的键值存储。 真正的基本例子:

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。

内部哈希实现可以在这里找到:

当哈希函数针对不同的值产生相同的结果时,这就是冲突。 大多数安全的哈希函数没有已知的冲突。

简短的哈希图理论

要存储键值对,最幼稚的方法是创建一个列表并遍历每个查找的所有对,复杂度为O(n)。 不好。

更高级的方法是创建平衡的搜索树并实现稳定的O(log(n))查找效率,这在许多情况下都是很好的。

好的,但是如果我们想做得更快呢? 最快的查找速度是O(1),我们使用数组的速度如此快。 但是我们没有适合数组的键。 解决方案是对键进行哈希处理,并使用哈希或哈希的一部分。

让我们创建BN存储桶。 每个存储桶都是一个简单的列表。 哈希函数将像这样转换**:hash(key)-> {1..BN}

向哈希表添加新的键值会将其插入到hash(key)存储桶中。

Golang地图的一些见解

查询时间

令PN为哈希图中的键值对的数量。

如果我们的PN≤BN且碰撞为零,这是理想情况,查找时间为O(1)。

如果PN> BN,则至少一个存储桶将至少具有2个值,因此查找可能需要2个步骤-找到存储桶,然后简单地遍历存储桶列表,直到找到键值对。

桶中平均项目数称为负载率。 负载因子除以2将是平均查找时间,而冲突率将定义其分散度。 形式上,复杂度仍然是O(n),但实际上冲突很少发生,并且**几乎均匀地分布到存储桶中。

Hashmap 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.Pointer
	value 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 * 3
	if 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 | Buckets

       9 |   1 |        2
      14 |   2 |        4
      27 |   3 |        8
      53 |   4 |       16
     105 |   5 |       32
     209 |   6 |       64
     417 |   7 |      128
     833 |   8 |      256
    1665 |   9 |      512
    3329 |  10 |     1024
    6657 |  11 |     2048
   13313 |  12 |     4096
   26625 |  13 |     8192
   53249 |  14 |    16384
  106497 |  15 |    32768
  212993 |  16 |    65536
  425985 |  17 |   131072
  851969 |  18 |   262144
 1703937 |  19 |   524288
 3407873 |  20 |  1048576
 6815745 |  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 bmap
	const dataOffset = unsafe.Offsetof( struct {
		tophash [bucketCnt] uint8
		cells   int64
	}{}.cells)

	t, h := mapTypeAndValue(m)

	fmt.Printf( "Overflow buckets: %d" , h.noverflow)

	numBuckets := 1 << h.B

	for r := 0 ; r < numBuckets*bucketCnt; r++ {
		bucketIndex := r / bucketCnt
		cellIndex := r % bucketCnt

		if cellIndex == 0 {
			fmt.Printf( "\nBucket %3d:" , bucketIndex)
		}

		// lookup cell
		b := (*bmap)(add(h.buckets, uintptr (bucketIndex)* uintptr (t.bucketsize)))
		if b.tophash[cellIndex] == 0 {
			// cell is empty
			continue
		}

		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  24

Overflow 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 | Buckets

       0 |  18 |   262144
 1000000 |  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 | Buckets

       0 |  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