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)
}

posted @ 2023-06-21 15:53  JL_Zhou  阅读(28)  评论(0编辑  收藏  举报