
Zig 重写 libc 致 SQLite 性能暴跌 12000 倍
来源标题:Shlemiel the Painter Is Back: How Zig's Libc Rewrite Made SQLite 12,000× Slower
Zig 语言重写 libc 后,导致 SQLite 在特定场景下性能下降约 12000 倍。问题源于类似“Shlemiel the Painter”的陷阱,即看似简单的实现选择带来了二次方时间复杂度。作者在其 Rust 编写的国际象棋分析服务中发现了该问题,该服务依赖 SQLite 存储分析结果。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
2001年,Joel Spolsky写过一篇题为《Back to Basics》的精彩文章,他的文字对我的职业生涯影响很大。那篇文章的核心观点是:从业者应该理解基础知识——无论我们看起来离驱动计算的基础算法多么抽象、多么遥远,它们都可能出人意料地对我们的项目产生巨大影响。
他举的例子之一就是“Shlemiel the Painter”陷阱:一个看似简单的选择,却导致了平方级的运行时复杂度。在那个寓言里,一个给街道画线的油漆工从不动自己的油漆罐。随着他沿着线越走越远,越来越多的时间都花在往返油漆罐的路上。
令人惊讶的是,这种 Shlemiel the Painter 模式反复出现¹,包括在2026年的现在。
几天前,我发帖介绍了我上线的一个国际象棋分析服务↗²。这主要是一个由 sqlite 支撑的 Rust 服务,负责把分析任务分发给 runner。每个 runner 会处理数百盘棋局,并把一大组 JSON 数据包返回给主服务,主服务处理后再塞进 sqlite。
虽然分析过程会让 runner 上的二十多个核心满载运行好几分钟,但存储数据的过程本应只需毫秒级——亚毫秒级。
可以想象,当我第一次处理真正大规模的对局集合时有多惊讶:把 JSON 数据插入数据存储这个简单过程——大概4万条记录——花了好几分钟。在我所有的测试中,这从未真正成为关键路径,但面对大量记录时,它突然成了一个致命问题。
更让人困惑的是,它完全占满了一个核心,是一个 CPU 密集型操作。把简单的 JSON 插入 SQLite 怎么会消耗数分钟的 CPU 时间?
Shlemiel the painter 现身了。剧透一下:他是通过 Zig 的 cc 编译器及其对标准库函数的替换被引入这个项目的。
这个项目不是用 Zig 构建的,而是用 Rust。sqlx 的 sqlite crate 需要为目标环境编译——sqlite 当然是一个 C 项目——所以为了方便跨平台构建,在这台机器上以便捷的构建和部署流程生成 Mac ARM64 以及 Linux ARM64 / x86-64 二进制文件,在面向 Linux 时会用 zig cc 来编译 sqlite,并通过 cargo-zigbuild 慷慨地提供跨平台目标支持。
在我的 Mac 上用一个构建和部署脚本,我就能面向所有平台,无需容器或构建机器。Zig 优雅的 libzigc 重新实现了标准库,因此你可以静态链接所需功能。
Zig 使用 musl(标准库重写版),但他们一直在用纯 Zig 逐块重写它(通过 libzigc),一次移除一个 C 依赖。
性能问题正是在这里引入的。
在 strspn 的重写中(用纯 Zig 变体替换 musl 版本),第一步会测量字符串的整个长度。在 strcspn、strpbrk 和 strstr 的重写中也能看到这一点。
这对 tokenizer 来说是个问题。tokenizer 接收一个巨大的字符串,一个常见的迭代步骤是跳过空白,获取新的字符串位置,然后反复重复这个模式。有了这些重写引入的 Shlemiel the Painter 模式,当它逐步处理正在 tokenize 的文本时,它会无休止地重新测量字符串的整个长度。
这就是导致我性能问题的原因,也是它出现在 Linux 上、却从未在我的 Mac 上出现的原因(在 Mac 上,sqlite 编译没有使用 zig)。
对一个1.2MB的文件进行 tokenize——这正是把 JSON 传给 sqlite 时发生的情况——在这种模式下大约慢了12,000倍。文件越大,减速呈平方级增长。
我对这些伟大工具的相关人员没有任何恶意或不敬,这些工具支撑了如此多的事情。但有趣的是,在2026年,这些老问题仍会重现,我们都需要保持警惕。如此微不足道的东西,却在最意想不到的琐碎之处,把一个进程磨到停滞。
我最初对这个问题的修复——至少在我自己的代码里——是预先从 JSON 中剔除空白,这大大降低了 sqlite3 中 tokenize 的成本,但因为不知道它还会在哪里咬人,我把 Linux 构建迁移到了 Linux 容器。
很多很多年前,当 Sunspider 还是首选基准测试、浏览器会宣扬自己的霸主地位时,同样的模式可以在 Firefox 处理字符串拼接的方式中看到,导致它在该基准测试中落后。Sunspider 的基准测试运行不知怎么地包含了大量字符串拼接——即使测试与字符串关系不大——而一次简单的基准运行 profile³ 显示,很大一部分运行时都消耗在不断地分配稍大一点的字符串内存、复制到新位置、然后重复这个模式上。这就是 Shlemiel the Painter。
我修正了这个问题(根据大小超额分配字符串缓冲区,使后续拼接可以复用缓冲区,直到它确实需要再次调整大小,而调整时又会超额分配),写了博客,Brendon Eich 注意到了,于是 Firefox 在该基准测试中取得了领先⁴。如此微不足道的一件事,把它从慢吞吞的家伙变成了基准测试的领跑者。↩
我并不幻想能靠一个小众的国际象棋分析服务养家糊口,最多也就是希望收支平衡,覆盖运行成本。我把它上线,是因为我发现这项工作对我自己的棋艺有帮助,并希望它偶尔能遇到合适的用户,同时不让我背上做这件事的成本负担。↩
Profiler 很棒。它们是我们可用的最强大的工具,所以对我来说很奇怪,它们竟然如此少被使用。↩
值得一提的是,字符串拼接在几乎所有 Web 项目中都极为常见,所以这在现实世界中的好处也是巨大的。当时有使用 string builder 的最佳实践,但人们常常只是在循环里把字符串硬拼在一起。↩
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。