Go 1.24 的 map 换成了 Swiss Table:从 8 槽 group 到 #81299 的无限 split
Go 1.24 用 Swiss Table 的设计替换了旧的 map 实现。
参考资料:
- VictoriaMetrics 原文:
- Go 官方博客:
- Issue:
运行时里 map 是什么
m := make(map[string]int),m 在运行时的表示是一个指向 internal/runtime/maps.Map 的指针。
type Map struct {
used uint64
seed uintptr
dirPtr unsafe.Pointer
dirLen int
...
}
used统计条目数;len(m)是 O(1),因为它直接读这个字段。seed每个 map 随机,因此同一批 key 在不同 map 里的排布可能不同。Go 用 map 的 seed 对 key 做哈希。
存储布局
三部分:directory、一个或多个 table,以及每个 table 内部的 group。group 是最小的存储单元。
Group
只含 1 个 group 的小 map:group 最多容纳 8 对键值,dirPtr 直接指向这个 group。每个 group 包含:
- 8 个槽位,放键值对。
- 8 个控制字节,每个槽位一个,合起来存在一个
uint64里(控制字)。
type group struct {
ctrl uint64
slots [8]struct {
key Key
elem Elem
}
}
控制字节与控制字
Go 用 seed 对 key 哈希,再把哈希切成两部分。在多数 64 位目标上,高 57 位是 H1,低 7 位是 H2。H2 存在活跃槽位对应的控制字节里。控制字节的最高位:若为 0,低 7 位就是 H2(活跃条目);若为 1,该字节是特殊状态:empty = 10000000,deleted(tombstone)= 11111110。查找遇到 empty 可以停下,遇到 deleted 必须继续。
定位或插入 key 时,Go 一次性把 H2 与 8 个控制字节比较。在 amd64 上 Go 用 SIMD 同时比较 H2 和 8 个控制字节,得到一个候选槽位位图;其他架构则对 64 位控制字做算术/位运算。之后只从候选槽位读出完整 key,做 == 相等判断。
Table
一个 group 只有 8 个槽位。超过之后,Go 把 group 数从 1 翻倍到 2,并引入管理一个或多个 group 的 table。
type table struct {
used uint16
capacity uint16
growthLeft uint16
...
groups groupsReference
}
起始 group = H1 % group 数。group 数变化时,Go 会为每个 key 重新计算。
一个 group 可能满了,另一个还有空槽。若某个 key 的起始 group 已满,Go 按三角探测序列检查其他 group:在 8 个 group 的 table 里从 group 3 出发,依次 +1、+2、+3……得到序列 3, 4, 6, 1, 5, 2, 0, 7(累计偏移是三角数 0,1,3,6,10,...)。group 数是 2 的幂,所以每个 group 都会被访问一次。三角探测避免聚集——线性探测 3,4,5,6,7,0,1,2 会形成一块不断变大的满区。
扩容机制:
- Load factor =(活跃 + 已删除)/ table 槽位数。最大 load factor 是 7/8 = 87.5%。
growthLeft记录新 key 还能消耗多少空槽。16 槽的 table 插入上限是 14 个条目(16*7/8=14)。 - 一个 table 可以持续翻倍 group,最多到 128 个 group = 1024 槽位。超过之后,Go 把这个 table 拆成 2 个新 table。
- 只有需要空间的 table 会被重建,其他 table 不变。split 用 H1 下一个尚未使用的高位来决定进两个新 table 中的哪一个;table 内部的 group 选择用 H1 的低位。
Directory
directory 是一个指向 table 的指针数组。H1 最左边的若干位选中一个 directory 条目,它告诉 Go 哪个 table 可能包含该 key。一个 table 可以单独 split,而不必让其他 table 一起 split。
Global depth 与 local depth
type Map struct {
dirPtr unsafe.Pointer
dirLen int
globalDepth uint8
...
}
type table struct {
localDepth uint8
...
}
globalDepth:用于选中 directory 条目的高位哈希位数(整个 directory)。localDepth:识别该 table 所需的高位哈希位数(每个 table)。
如果某个 table 的 localDepth >= 2
}
}
return keys
}
func main() {
fmt.Println("Go version:", runtime.Version())
m := make(map[Key]struct{})
// 896 个 key 成功,填满初始 table
for _, k := range makeKeys(896) {
m[k] = struct{}{}
}
fmt.Printf("Inserted 896 keys successfully (map len=%d)\n", len(m))
// 插入第 897 个同哈希 key 会触发无限 split 循环与 OOM
fmt.Println("Inserting 897th key...")
m[makeKeys(897)[896]] = struct{}{}
fmt.Println("Finished (unreachable)")
}
只影响同时满足以下条件的应用:map 的 key 是一组 interface 的集合(例如 `map[[5]any]T`);数据来自类似 `encoding/gob` 的序列化器,且不受信任的客户端能选择具体类型(混用 `int(0)`、`uint(0)`、`int64(0)`、`uint64(0)`)。