把 BadgerDB 移植到仓颉(二):十次优化与一次误判
内容提要
文章介绍将 BadgerDB 移植到仓颉时的性能优化与一次误判。作者列举十项优化,包括 CRC32C 全局查找表、用 Condition 替代轮询、将 flush 移出写锁使写吞吐翻倍。随后复盘一起“编译器 bug”乌龙:实为仓颉 Array 切片是共享视图而非拷贝,导致自造错误。结论是怀疑编译器前应先检查自身代码的别名与共享语义。
延伸解读
性能优化中的可复现性
文章强调,优化效果必须能在同一台机器上重跑才算数。作者对多数优化只保留定性描述,因为旧实现无法重跑,避免引用不可复现的具体数值。这提醒读者,在评估性能改进时,应关注可复现的基准测试,而非孤立的百分比数字。
写吞吐与关闭时间的权衡
将 flush 移出写锁后,运行期写吞吐约翻倍,但 close 耗时增加。作者认为这是正确权衡,因为运行期吞吐是常态成本,而 close 只发生一次。这体现了性能优化中区分高频与低频操作的重要性,将成本从每次操作转移到关机时刻。
切片视图语义的陷阱
仓颉的 Array 切片是共享底层存储的视图,而非拷贝。修改切片元素会直接影响原数组。这一语义容易导致意外别名,尤其在用切片做写回探测时。开发者需显式复制来获得独立副本,避免因共享存储引发难以察觉的错误。
怀疑编译器前的自检
作者误将自身代码错误归咎于编译器,最终发现是切片视图语义导致。这提醒我们,当现象诡异时,应先检查代码中的别名与共享语义。上报被驳回并非徒劳,它促使作者厘清语言语义,并区分真伪问题,提升排查能力。
Q&A
把 BadgerDB 移植到仓颉时,有哪些显著的性能优化?
文章列举了十项优化,包括:CRC32C 全局查找表、LogEntry::encode 批量拷贝、用 Condition 替代轮询、移除 updateDiscardStats、WriteRequest 对象池化、Arena 批量拷贝、Node.compareKey 免建对象、Skiplist.add 内联、AtomicStorage 分段扩展、让 flush 离开写锁。
CRC32C 优化具体是怎么做的?效果如何?
优化前每次校验都重建 256 项查找表,优化后改为全局查找表,进程内只建一次。效果是从剖析热点降到可忽略。
为什么用 Condition 替代轮询能提升性能?
轮询等待有 1 毫秒周期,大事务需要多轮检查,延迟按轮数累积。改用 Condition 后,等待方被精确唤醒,大事务等待从轮询周期量级降到即时唤醒。
让 flush 离开写锁带来了什么好处和代价?
好处是运行期写吞吐约翻倍,实测中位数 97,370 次/秒。代价是 close 耗时变慢(54 毫秒),因为要等在途的表落盘完毕。但这是正确的权衡,因为运行期吞吐是常态成本,close 只发生一次。
作者遇到的“编译器 bug”乌龙是怎么回事?
作者发现 CRC32C 在 -O2 下行为异常,做了三重实验后认为是编译器缺陷并上报。但官方回复是误报,根因是作者自己的复现代码违反了仓颉语义:Array 的切片是共享底层存储的视图,不是拷贝,导致写切片元素时修改了原数组,制造了错误现象。
从这次乌龙中得出了哪些教训?
三条教训:1. 仓颉的 Array 切片是视图,不是拷贝,写切片元素会直接影响原数组;2. 怀疑编译器之前,先怀疑自己代码里的别名与共享语义;3. 上报被驳回不是白费,能搞清真实语义。