把 BadgerDB 移植到仓颉(二):十次优化与一次误判

💡 原文中文,约3900字,阅读约需10分钟。
📝

内容提要

文章介绍将 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. 上报被驳回不是白费,能搞清真实语义。

🏷️

标签

➡️

继续阅读