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 4 | 8 字节 | c9e4a2aa07 | HashInt64Speed -10%,MapAccessMiss/int64/len=65536 -10.9%,MapGroupBy -10% |
| Apple M1 Max | 16 字节 | b751a977cb | Hash5 -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里也提到了这一点。
五、对业务代码的影响
- 依赖 hash 顺序或碰撞行为的代码:接口 key 的 typehash 折叠改变了
map[any]any的碰撞情况,任何按遍历顺序、桶分布或「已知这两个 key 会撞」来推理的逻辑都要重新验证。 - 把 int / uint 混用当 key 的代码:
int64(0)与uint64(0)在接口里不再等价,原先靠碰撞「碰巧」合并的行为不会继续。 - 跨版本基准对比会失真:
Hash5、HashInt64Speed、MapGroupBy/int64、MapAccessMiss/int64/len=65536这些基准在 1.28 前后测出的数字不可直接横向比较,尤其是小键占比高的场景,需要按新阈值重新建立基线。