把 BadgerDB 移植到仓颉(四):怎样测才算数
内容提要
本文是BadgerDB移植仓颉系列终篇,聚焦基准测试方法论。作者复盘了四个缺陷:数据目录误落tmpfs导致I/O虚高、负载过小无统计意义、缺少环境指纹、跨语言规模不对齐;并揭示Go侧三个陷阱:frac_10读空表、key长度不等价、语义对齐基准缺失。最终给出可复现流程:只报绝对值与同二进制开关对照、必报离散度、环境指纹入报告、一条命令跑完,并指出原版本演进表无git依据。
延伸解读
基准测试的常见陷阱:从数据目录到负载规模
文章指出四个方法学缺陷:数据目录误落tmpfs导致I/O虚高、负载过小无统计意义、环境指纹缺失、跨语言规模不对齐。这些缺陷不会表现为数值偏差,而是直接给出错误结论。例如,tmpfs上的SSTable写入速度是内存速度,而非磁盘速度;64毫秒的负载单次测量波动可达±32%,点值不可靠。读者在设计基准时,应检查数据目录文件系统、确保负载足够大、记录环境指纹,并显式对齐跨语言对照的规模。
Go侧基准的隐藏问题:空表、key长度与语义缺失
文章揭示Go侧三个陷阱:frac_10读的是永久空表,命中率为0,不能作为读基准;key长度不等价,Go实际比较16字节而仓颉为8字节,影响比较分支与cache;Go侧缺少语义对齐的并发读和事务写基准。因此,跨语言对照仅在纯并发写上干净。读者在引用跨语言数据时,需确认基准语义是否对齐,避免将空表遍历或不同key长度的结果直接比较。
可复现流程的核心原则:绝对值、离散度与环境指纹
文章提出一套可核验流程:只报当前revision的绝对值和同一二进制内的开关对照,不做跨版本对比;必须报告离散度,>15%的项标记为波动大;环境指纹(CPU、内核、编译器版本、commit、文件系统、轮次)必须入报告;一条命令跑完全部。这些原则确保数字可重跑、可核验。读者在评估性能数据时,应关注离散度和环境信息,而非孤立的点值。
版本演进表的教训:跨版本对比需有git依据
文章发现原版本演进表存在两个问题:v1.3.1 tag不存在,优化提交发生在最早tag之前。因此,表头的版本对照关系没有历史依据。这引出了“不做跨版本对比”的原则:版本对照要求每个版本都能检出、构建并在同一环境下运行。读者在引用版本性能对比时,应核实版本是否真实存在且可复现,避免基于手工标注的虚假对照。
Q&A
BadgerDB 移植仓颉的基准测试中,四个方法学缺陷是什么?
四个缺陷是:1. 数据目录落在 tmpfs 上,导致 I/O 成绩虚高一个数量级;2. 负载太小,单次测量没有统计意义;3. 环境指纹缺失,数字无法复核和横向比较;4. 跨语言对照的规模不对齐。
为什么 Go 的 frac_10 基准不能用来对照仓颉的并发读?
因为 frac_10 的 readFrac = 1.0,只 Get 从不 Put,表永远为空,命中率 0。它实际是在空表上走一趟 head 比较,不能与仓颉预写满 5000 条后再并发读对照。
如何解决数据目录落在 tmpfs 导致 I/O 成绩虚高的问题?
不改被测源码,用私有 mount namespace 重定向:unshare -rm bash -c "mount --bind <真实盘目录> /tmp && exec <基准命令>"。同时需将 LD_LIBRARY_PATH 指向 CANGJIE_HOME 的真实路径,避免软链被遮住导致找不到库。
现在项目采用的基准测试流程包含哪些关键步骤?
流程包括:1. 只产出当前 revision 的绝对值和同一二进制内的开关对照,不做跨版本对比;2. 必须报离散度,>15% 的项自动标记;3. 环境指纹进报告头(CPU/内存/内核/编译器版本/commit/数据目录文件系统/轮次);4. 一条命令跑完全部:python3 scripts/bench/run-bench.py --runs 5。
为什么原报告的版本演进表是错误的?
核对 git 历史发现:v1.3.1 这个 tag 在仓库里不存在(最早的是 v1.2.0);表里的优化提交集中在 2026-03-11 ~ 03-14,而最早的 tag 是 2026-03-21,优化发生在第一个 tag 之前。因此表头的版本对照关系没有依据。
并发跳表基准测试的当前成绩如何?
并发跳表(5000 ops / 16 线程,30 轮):并发写中位数 387 ns/op(约 258 万 op/s),离散度 ±40%;并发读中位数 333 ns/op(约 300 万 op/s),离散度 ±39%;顺序写 555 ns/op,±39%;顺序读 1,227 ns/op,±27%。负载仅约 64 毫秒,点值不稳定,正确说法是“亚微秒级”。
批量写对每 key 成本的摊薄效果如何?
同一写路径,40 万 key / 8 线程:单键事务每 key 成本 6,778–6,857 ns;8 key/事务 2,216–2,494 ns,快 2.7–3.1 倍;64 key/事务 1,715–1,969 ns,快 3.5–4.0 倍。写路径的同步成本无法靠框架内部批量提交摊薄,真正的杠杆在用户侧的事务粒度。