编程 Go 1.24 的 map 换成了 Swiss Table:从 8 槽 group 到 #81299 的无限 split

2026-10-05 21:31:39

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)`)。
复制全文 生成海报 Go SwissTable map 运行时 哈希表 性能

推荐文章

程序员茄子在线接单