别着急,坐和放宽
使用社交账号登录
hmap:Map 的头节点,保存了 count、B、buckets 指针等。bmap (Bucket):每个桶固定存储 8 个键值对。为了内存对齐,它将 8 个 tophash 放在一起,接着是 8 个 key,最后是 8 个 value。overflow:当桶满时,通过 overflow 指针挂载一个新的 bmap。| 维度 | 拉链法 (1.24之前) | 开放寻址法 (Swiss Table, 1.24+) |
|---|---|---|
| 内存布局 | 离散(溢出桶通过指针连接) | 紧凑(数据在 Group 内连续存储) |
| CPU 缓存 | 频繁 Cache Miss(追踪指针) | 缓存友好(利用 Cache Line) |
| 冲突处理 | 挂载链表(拉链) | 二次探测(寻找新插槽) |
| 查找加速 | 逐个对比 tophash | SIMD 并行匹配 8 个插槽 |
| 空间利用率 | 较低(存在大量指针和空闲槽位) | 较高(负载因子从 6.5 提升至 7/8) |
| 性能上限 | 受限于内存延迟 | 接近硬件极限(单指令多数据) |
Go 1.24 弃用了 bmap,引入了 Group 和 Metadata 的概念。
Map 的头部结构体依然叫 hmap,但内部字段发生了变化:
这是 Swiss Table 的精髓。每个插槽不再只靠 tophash 过滤,而是使用 1 个字节(8 位)的控制位:
0b10000000 (0x80):Empty。表示此槽位完全为空。0b11111110 (0xfe):Deleted(墓碑标记)。表示元素已删,但探测链不能断。0b0xxxxxxx (h2):Occupied。最高位为 0,后 7 位存储哈希值的低 7 位,称为 h2 指纹。在查找一个 Key 时,Go 1.24 的逻辑如下:
h1(高 57 位)和 h2(低 7 位)。h1 找到对应的 Group。PCMPEQB)将这 8 个字节与目标的 h2 指纹进行比较,瞬间得到一个掩码。equal 比较。为什么快? 因为它把原本需要 8 次循环的比较,压缩成了一次指令和一次位运算,且元数据与数据在内存布局上高度紧凑。
由于不再有溢出桶,当某个 Group 满员且指纹不匹配时,Go 1.24 使用一种特殊的二次探测——三角形探测(Triangular Probing)来寻找下一个 Group:
p(i) = (h1 + i*(i+1)/2) mod 2^B墓碑标记(Tombstone):在删除元素时,槽位会被标记为 Deleted (0xfe)。这是为了告诉查找流程:“这里虽然没数据,但探测链在此并未中断,请继续往后找”。只有遇到真正的 Empty (0x80) 才会停止探测。
扩容依然是渐进式(Incremental)的。当触发扩容(负载因子超标或探测链过长)时:
mapassign 或 mapdelete 操作时,搬迁旧表中的 Group 到新表。map 的迭代顺序依然是伪随机的,不要依赖它。Go 1.24 的 Map 重构是工程领域“榨干硬件性能”的典范。它通过 Swiss Table 将算法(开放寻址)与硬件(SIMD、Cache Line)深度绑定。对于开发者而言,这不仅意味着更快的执行速度,更标志着 Go 语言在追求极致性能的道路上又迈出了坚实的一步。
type hmap struct {
count int // 元素总数
flags uint8 // 状态位(写入中、迭代中等)
B uint8 // 指数级大小,2^B 表示 Group 的对数
hash0 uint32 // 哈希种子,对抗哈希攻击的关键
// 核心存储指针
table unsafe.Pointer // 指向当前的存储表(包含元数据数组和数据数组)
oldtable unsafe.Pointer // 扩容时的旧表指针,用于增量搬迁
// ...
}