(四)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...

相关推荐
掘金者阿豪2 小时前
VibeVoice告别单调配音!加上cpolar实现多角色语音随时随地搞创作
后端
再吃一根胡萝卜2 小时前
pip install -e 让你的Python包开发效率翻倍
后端
神奇小汤圆2 小时前
Claude Code 装上这 10 个 Skills,直接起飞!
后端
苏三说技术2 小时前
高并发下的分布式ID生成架构
后端
newbe365242 小时前
C# 后端集成 CodeBuddy CLI 实战指南
后端
chushiyunen2 小时前
django venv虚拟环境
后端·python·django
m0_738120722 小时前
网络安全编程——PHP基础Cookie详细讲解
后端·安全·web安全·前端框架·php
lclcooky2 小时前
Spring中的IOC详解
java·后端·spring
天才梦浪2 小时前
wsl的网络导致springboot启动提示端口占用
网络·spring boot·后端