GO 语言 map 的理解
GO 语言 map 的理解
map 的底层实现原理是怎么样的?
map 的底层实现是一个哈希表的桶数组,每个桶都是一个链表,用于存储哈希值相同的键值对。当我们需要查找一个键时,首先计算出其哈希值,然后找到对应的桶,遍历链表查找对应的键值对。
但是,如果桶中的链表过长,遍历链表的时间复杂度会变得很高,影响查找效率。为了解决这个问题,Go 语言中的哈希表采用了链表和平衡树相结合的方式,称为“链表+平衡树”。
具体来说,当一个桶中的链表长度超过一定阈值时,Go 语言会将这个桶中的键值对转移到一个平衡树中。平衡树的查找效率比链表高,可以提高查找效率。当桶中的键值对数量减少到一定程度时,Go 语言会将平衡树转换回链表,以节省内存空间。
下面是一个示例代码,用于演示 map 的“链表+平衡树”实现原理:
func main() {
m := make(map[int]int)
for i := 0; i < 100000; i++ {
m[i] = i
}
fmt.Println(m[99999])
}
在这个示例代码中,我们创建了一个 map,向其中添加了 100000 个键值对。接着,我们查找了键为 99999 的值。在运行这个程序时,输出应该是:99999
接下来,让我们来看一下这个示例代码的底层实现原理。
首先,当我们创建一个 map 时,Go 语言会为其分配一块内存空间,用于存储桶数组和其他相关信息。在这个示例代码中,我们使用了 make 函数来创建 map,其实现原理如下:
func make(mapType *mapType, hint int) *hmap {
...
// 分配内存空间
h := new(hmap)
...
// 初始化桶数组
h.buckets = newarray(uintptr(h.bucketsize), bucketSize)
...
return h
}
在这个函数中,我们可以看到,首先会分配一块内存空间,然后初始化桶数组。桶数组的大小是根据 hint 参数计算出来的,这个参数可以用来指定 map 中预期存储的键值对数量。
接着,当我们向 map 中添加键值对时,Go 语言会先计算出键的哈希值,然后根据哈希值找到对应的桶。如果桶中已经存在相同的键,则更新其对应的值;否则,将新的键值对添加到桶的末尾。
在这个示例代码中,我们向 map 中添加了 100000 个键值对,其实现原理如下:
func mapassign(t *maptype, m *hmap, key unsafe.Pointer) unsafe.Pointer {
...
// 计算哈希值
hash := t.hasher(key, uintptr(m.hash0))
...
// 找到对应的桶
bucket := hash & (uintptr(m.bucketsize) - 1)
...
// 遍历链表查找键值对
for p := &buck.hash; *p != nil; p = &(*p).next {
...
// 如果找到相同的键,则更新其对应的值
if t.key.alg.equal(key, (*p).key) {
...
return (*p).value
}
...
}
...
// 如果没有找到相同的键,则添加新的键值对到桶的末尾
new := newobject(t)
...
*p = new
...
// 如果桶中的链表长度超过一定阈值,则将其转移到平衡树中
if bucket.overflow(t) {
bucket.treeptr = newobject(t)
bucket.treeptr.settreeptr()
bucket.treeptr = treeptr(t, bucket.treeptr)
bucket.treeptr = bucket.treeptr.insert(t, m, key, new.value)
return new.value
}
...
return new.value
}
在这个函数中,我们可以看到,首先会计算出键的哈希值,然后找到对应的桶。接着,遍历链表查找键值对。如果找到相同的键,则更新其对应的值;否则,添加新的键值对到桶的末尾。如果桶中的链表长度超过一定阈值,则将其转移到平衡树中。
最后,当我们需要查找一个键时,Go 语言会先计算出其哈希值,然后根据哈希值找到对应的桶。如果桶中是链表,则遍历链表查找对应的键值对;如果桶中是平衡树,则在平衡树中查找对应的键值对。如果找到了,则返回其对应的值;否则,返回零值。
在这个示例代码中,我们查找了键为 99999 的值,其实现原理如下:
func mapaccess1(t *maptype, m *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {
...
// 计算哈希值
hash := t.hasher(key, uintptr(m.hash0))
...
// 找到对应的桶
bucket := hash & (uintptr(m.bucketsize) - 1)
...
// 如果桶中是链表,则遍历链表查找键值对
if !bucket.needkey() {
for p := *buck; p != nil; p = p.next {
...
// 如果找到相同的键,则返回其对应的值
if t.key.alg.equal(key, p.key) {
...
return p.value, true
}
...
}
} else {
// 如果桶中是平衡树,则在平衡树中查找键值对
...
}
...
// 如果没有找到相同的键,则返回零值
return nil, false
}
在这个函数中,我们可以看到,首先会计算出键的哈希值,然后找到对应的桶。如果桶中是链表,则遍历链表查找对应的键值对;如果桶中是平衡树,则在平衡树中查找对应的键值对。如果找到了,则返回其对应的值;否则,返回零值和 false。
map 中针对 string 的 hash 过程是怎么样的?
map 的哈希过程是由哈希函数完成的。哈希函数的作用是将任意长度的输入(比如一个字符串或一个数字)映射为固定长度的输出(比如一个 32 位或 64 位的整数),这个输出就是哈希值。
在 Go 语言中,map 的哈希函数是由 runtime.hashmapstr 函数实现的。这个函数的实现比较复杂,但是我们可以简单了解一下它的工作原理。
首先,runtime.hashmapstr 函数会根据输入的字符串计算出一个初始的哈希值。这个初始的哈希值是一个随机数,用于增加哈希函数的随机性,防止哈希冲突。
接着,runtime.hashmapstr 函数会遍历输入的字符串,将每个字符的 ASCII 码值乘以一个固定的常数,然后将结果累加到初始的哈希值中。这个常数是一个随机数,用于增加哈希函数的随机性,防止哈希冲突。
最后,runtime.hashmapstr 函数会将累加后的哈希值进行一些位运算和取模操作,得到最终的哈希值。这个最终的哈希值是一个无符号整数,用于确定键值对在 map 中的位置。
func hash(s string) uint64 {
var h uint64 = 14695981039346656037
for i := 0; i < len(s); i++ {
h = (h ^ uint64(s[i])) * 1099511628211
}
return h
}
func bucket(h, size uintptr) uintptr {
return h & (size - 1)
}