编程 Go 1.28 的 map 哈希改动:小键改走 wyhash,int64(0) 和 uint64(0) 不再碰撞

2026-11-03 21:01:18

Go 1.28 的 map 哈希改动:小键改走 wyhash,接口 key 的 int64(0) 与 uint64(0) 不再碰撞

Go 1.28 在 map 哈希上有两处会影响实际行为的改动:小于 MinAeshashSize 的键不再走 AES hash 改用 wyhash;接口哈希把动态类型的 compiler typehash 折了进来。前者影响基准数字和吞吐量,后者直接影响 key 的碰撞与去重语义。

一、UseAeshash 换成可调的 MinAeshashSize

原来控制是否启用 AES hash 的是 UseAeshash,现在换成了可调的 MinAeshashSize。原因是 AES 的 SIMD 实现「不喜欢被用于一次性小输入」——对小键做一次 hash 时,走 AES 路径并不划算。低于这个阈值的键改用 wyhash。

阈值按微架构分别设定:

平台阈值相关 CL观测到的变化
Zen 48 字节c9e4a2aa07HashInt64Speed -10%,MapAccessMiss/int64/len=65536 -10.9%,MapGroupBy -10%
Apple M1 Max16 字节b751a977cbHash5 -41%,MapGroupBy/int64 -65%

M1 Max 上的降幅明显更大,Hash5 这种 5 字节小键正好落在阈值以下,从 AES 换到 wyhash 后收益最直接。

二、接口哈希折入动态类型的 typehash

接口 key 的哈希现在会折叠动态类型的 compiler typehash,带来的直接效果是:map[any]any 里的 int64(0) 和 uint64(0) 不再碰撞(40e7643c46)。

在此之前,这两个值放进 map[any]any 会因为哈希值相同而落到同一桶路径上;改动之后它们按类型区分开。这是语义层面的变化,不只是性能问题。

三、Swiss-table map 与其他收尾改动

GOEXPERIMENT=swissmap 已经成为唯一实现。因为 wrapper 的开销在合成基准里能看出来,通过提高 inline budget 把 mapaccess2 内联进了 mapaccess1(defa1f0155)。小键在低于 MinAeshashSize 时从 AES 切到 wyhash,用的就是上面两个 CL(c9e4a2aa07、b751a977cb)。

四、遗留问题

  • 其他微架构(Intel 各 stepping)的 cutoff 尚未逐个测定,属于容易跟进的后续工作。
  • arm64 上延迟导向的 cutoff 192 还没探索,见 go.dev/issue/80970,b751a977cb 里也提到了这一点。

五、对业务代码的影响

  1. 依赖 hash 顺序或碰撞行为的代码:接口 key 的 typehash 折叠改变了 map[any]any 的碰撞情况,任何按遍历顺序、桶分布或「已知这两个 key 会撞」来推理的逻辑都要重新验证。
  2. 把 int / uint 混用当 key 的代码:int64(0) 与 uint64(0) 在接口里不再等价,原先靠碰撞「碰巧」合并的行为不会继续。
  3. 跨版本基准对比会失真:Hash5、HashInt64Speed、MapGroupBy/int64、MapAccessMiss/int64/len=65536 这些基准在 1.28 前后测出的数字不可直接横向比较,尤其是小键占比高的场景,需要按新阈值重新建立基线。
复制全文 生成海报 Go runtime 数据结构

推荐文章

程序员茄子在线接单