
这是 Daniel Lemire 的新作 constmap。如果你手里有一组固定的字符串键,需要反复查询对应的数字,传统 map(映射)的内存开销会让你肉疼——每个键值对动辄几十字节的元数据,缓存行(CPU 缓存的最小传输单位)里塞不下几个条目。
constmap 的解法很刁钻:用 binary fuse filter(二进制熔丝过滤器)把键值对压进一个紧凑数组,查询时 1 次哈希计算、3 次数组访问、2 次异或运算,完事。
从布隆过滤器到熔丝过滤器:一场 " 误报 " 的退位
布隆过滤器(Bloom Filter)是老熟人——用多个哈希函数把元素映射成位图,查询时可能把 " 不存在 " 错判为 " 存在 ",这叫假阳性(False Positive)。它省内存,但有两个硬伤:不能存值,只能回答 " 在不在 ";假阳性无法消除。
2019 年,Lemire 和 Thomas Mueller Graf 提出 xor filter(异或过滤器)。思路突变:用 3 个哈希函数定位 3 个槽位,把键的指纹异或进这 3 个位置。查询时取 3 个槽位异或,若结果匹配指纹则存在。关键是,构造算法能保证零假阳性——只要成功构建,查询结果绝对可靠。
2022 年,同一团队再进一步,binary fuse filter(二进制熔丝过滤器)把空间效率推到极致。constmap 正是基于这篇 ACM 期刊论文的实现,每个键值对仅需约 9 字节。

Go 的 map 底层是哈希表,每个桶(bucket)存 8 个键值对,溢出时用指针链下去。字符串键本身还要堆分配,指针、长度、容量头信息一个不少。实测中,百万级字符串 map 的内存占用轻松突破百 MB。
constmap 的构造是一次性的:传入键数组和值数组,算法在后台解一个线性方程组,把每个键的 " 位置信息 " 编码进紧凑数组。构造完成后只读,查询路径完全无分支、无指针追逐。
Lemire 在代码注释里写得很直白:「Looking up a key that was not in the original set returns an undefined value.」查不存在的键?返回未定义值。这是性能换安全的取舍——如果你需要区分 " 不存在 " 和 " 值为 0",得用 VerifiedConstMap,内存翻倍到约 18 字节 / 键,多存一个指纹做校验。
VerifiedConstMap 的哨兵值是 0xFFFFFFFFFFFFFFFF,64 位全 1。这个设计很 Go:uint64 的最大值作为 " 未找到 " 标记,既不牺牲零值语义,又给合法值留足空间。
磁盘序列化:把构造代价摊到一次
binary fuse filter 的构造不是免费的。算法要迭代求解,大数据集可能耗时数秒。constmap 的应对很工程化:SaveToFile/LoadFromFile 支持磁盘序列化,格式带 FNV-1a 校验和防损坏。

一个典型场景:微服务的配置中心。启动时拉取全量配置,构造 constmap,本地持久化。下次启动直接加载,跳过构造。查询性能接近数组随机访问,内存占用却比原生 map 低一个数量级。
代码示例里藏着细节:键必须唯一,键值切片长度必须相等。构造失败会返回 error,但成功后的 ConstMap 是 immutable(不可变的)——没有 Delete,没有 Update,线程安全由 " 只读 " 天然保证。
Lemire 的学术背景在这类工具库里体现得很充分。从 simdjson 到 fast_float,再到现在的 constmap,他的套路一致:找到数据访问的瓶颈点,用算法 +SIMD(单指令多数据)或紧凑数据结构硬刚过去。不追求通用性,只求特定场景下的极限性能。
constmap 的 README 末尾留了钩子:「See also the earlier xor filter paper」。如果你只需要判断存在性而不需要取值,xor filter 更省空间。这是产品化的克制——不替用户做选择,把决策权交还。
82 个 Star 里有多少人会真的用上?大概率不多。但那个需要处理亿级静态键值对、内存预算卡死的团队,会感谢这个库的存在。
你的项目里,有多少 " 只读一次、查询千万次 " 的数据集,还在用原生 map 硬撑?