(四)Go Map的SwissTable实现

Map2

一、SwissTable

为了进一步的优化性能、缓存命中率,Go从桶换成了SwissTable

SwissTable就是一个map有多个group,每个group有16个槽位,每个槽位有一个 metadata与之对应

给一个key,进行hash之后。得到一个64比特的数。

  1. 哈希值的高57比特被称为H1,用于定位 哪个 Group
  2. 低7比特被称为H2。放在metadata中存储,用于快速过滤。
  3. 左侧再加一位,用于特殊含义:如果是1000 0000,说明是空,如果是1111 1110,说明是deleted / tombstone,如果是0xxx xxxx说明满了

SIMD

Swiss Table 最大优势在 SIMD(Single Instruction, Multiple Data),一条指令,同时处理多个数据。

在过滤metadata中,我们不是for循环逐个比较,而是使用SIMD方法一次性比较16个。

  1. CPU提供了向量寄存器,可以在一条指令中比较多个字节
  2. 查询时,先将目标 Key 的 H2 广播填充至整个向量寄存器
  3. 再将 Group 的 16 个 Metadata 一次性加载进另一个寄存器。
  4. 利用 SIMD 指令让这两组数据一触碰,就能瞬间筛选出所有匹配的 Slot

二、Go 1.24实现

Go Map 1.24做了点适配:

  • 核心架构 :为了避免大 Map 在扩容时因"全量搬迁"导致的瞬间卡顿,Go 将 Map 拆分成了多个独立的 Table
  • 路由中心 :Map 持有一个 Directory(目录),它本质上是一个指向各个 Table 的指针数组。
  • 优化: 当元素非常小时,Directory会指向一个单独的组Group,并将表长度置为0。

1. GlobalDepth和LocalDepth

Directory.GlobalDepth 决定了目录的大小,即 2^GlobalDepth 个槽位。

  • 查找时,取哈希值的高几位作为索引去 Directory 里"查表"。
  • 重点:不同的 Directory 条目可能指向同一个物理 Table。当某个 Table 满了,它会独立分裂,而其他 Table 保持不动。

Table.LocalDepth 决定了具体的group在目录占了几行,与扩容有关,后面再讲

2. 查找流程

  1. 根据哈希值的高位构造探测序列
  2. 循环遍历
    1. SIMD快速匹配候选人
    2. 逐个比较候选人的键
      1. 如果找到,返回值
    3. 如果未找到,看是否有空元素
      1. 有则说明探测终止,直接返回未找到
      2. 无空元素,那就根据探测序列跳到下一个Group,继续找

3. 修改、增加流程

  1. 根据哈希值的高位构造探测序列
  2. 循环遍历
    1. SIMD快速匹配候选人
    2. 逐个比较候选人的键
      1. 如果找到,直接更新,退出
    3. 如果未找到,看是否有墓碑
      1. 有则记录第一个墓碑在哪,继续找
    4. 未找到,并且到达终点,优先填坑墓碑,不行就填坑空位
    5. 没位置则进行扩容

4. 删除流程

  1. 根据哈希值的高位构造探测序列
  2. 循环遍历
    1. SIMD快速匹配候选人
    2. 逐个比较候选人的键
      1. 如果找到, 是指针就清零, 不是指针就清内存
      2. 未找到,则继续下一个Group,直到到达终点
    3. 清空后,看组里有没有空位
      1. 有,说明是探测序列终点,可以置metadata为空
      2. 没有,说明组内满员,不是终点,置为墓碑(ctrlDeleted)

5. 扩容流程

计算当前容量*2是否小于规定的单表最大限制

  1. 小于,则翻倍扩容,分配一个两倍大的新表,把旧数据重新哈希搬过去,替换旧表
  2. 否则分裂扩容,创建两个最大容量的新表,数据分流搬迁,看是否需要扩容目录

Table.LocalDepth 决定了具体的group在目录占了几行 计算公式:指向同一个 Group 的目录项数量 = 2^(GlobalDepth - LocalDepth)

6. 边遍历边修改

由于 Go 慷慨的迭代语义,迭代是 Map 中最复杂的部分。规范中的语义摘要:

  1. 在迭代期间添加和/或删除条目绝不能导致迭代多次返回同一个条目。
  2. 在迭代期间添加的条目可能会被迭代返回。
  3. 在迭代期间修改的条目必须返回其最新值。
  4. 在迭代期间删除的条目绝不能被迭代返回。
  5. 迭代顺序是未指定的。在实现中,它是被显式随机化的。

直接遍历旧表,然后读值的时候读新表

参考资料

tonybai.com/2024/11/14/...

draven.co/golang/docs...

相关推荐
candyTong21 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
GetcharZp1 天前
GitHub 2.4 万 Star!D2 正在重新定义程序员画图方式
后端
zhangxingchao1 天前
多 Agent 架构到底怎么选?从 Claude Agent Teams、Cognition/Devin 到工程落地原则
前端·人工智能·后端
IT_陈寒1 天前
SpringBoot那个自动配置的坑,害我排查到凌晨三点
前端·人工智能·后端
ServBay1 天前
OpenCode 和它的7款必备插件
后端·github·ai编程
ping某1 天前
逐字节拆解 tcpdump
后端
阿凡9807301 天前
花 100 dollar,用 Claude 打通 EasyEDA&Fusion 双向同步
后端·程序员
irving同学462381 天前
从零搭建生产级 RAG:Embedding、Chunking、Hybrid Search 与 Reranker
前端·后端
她的男孩1 天前
从零搭一个企业后台,为什么我把能力拆成 Starter 和 Plugin
java·后端·架构
胡志辉1 天前
本地 AI 编码助手从 0 配起来:先选模型,再接 Ollama、VS Code、Claude Code 和 Codex
前端·后端