开发者 Simon Willison 在遛狗时用手机端 Claude (Opus 5.5) 完成了一项跨语言基准测试,对比用 C 和 Rust 编写 SQLite 可加载扩展 parse_number() 的性能与体积。该扩展用于剥离字符串中的 ,、$、£ 与 % 并解析数字。测试结果经历了一次戏剧性反转,最初调用标准库 strtod() 的普通 C 版本在纯小数解析上耗时 106.3 纳秒/行,慢于两个 Rust 版本(54.5 纳秒与 46.5 纳秒);但当 Claude 按指令手写了一套 C 语言快速小数解析函数后,仅 14 KB 的新 C 扩展将单行耗时压到了 16 至 19 纳秒,反超约 300 KB 的 Rust 扩展近一倍。

账面数字很容易让人得出手写 C 依然能在性能上碾压 Rust 的直白结论。但翻开实验底层的 GitHub 仓库源码就会发现,这场测试上半场考的根本不是语言速度,而是标准库底层算法的代差;下半场 C 语言的反超,则是用一段牺牲了完备性的窄化快车道,把测试变成了对 Rust 扩展框架 FFI 胶水层开销的摸底。

106 纳秒对 46 纳秒:普通 C 输在 glibc 的历史包袱

SQLite 处理带有货币符号或千分位的非规范数字文本时,在 SQL 层级嵌套调用 replace() 开销极大,编写可加载扩展是常见的优化手段。Willison 让 Claude 生成了四个实现,分别是两个 C 版本,以及基于 rusqlite(自 2023 年 12 月发布的 0.30.0 版本起支持直接编写可加载扩展)和 sqlite-loadable-rs 的两个 Rust 版本。

glibc 的历史包袱让普通 C 在起跑线上输给现代浮点算法(示意图)
glibc 的历史包袱让普通 C 在起跑线上输给现代浮点算法(示意图)

在 200 万行数据的基准测试中(扣除最快的 sum(length(v)) 基准耗时,15 轮交叉测试取最快值),四个扩展的精确净耗时与产物大小如下:

扩展实现编译体积混合数据 (ns/行)纯整数 (ns/行)纯小数 (ns/行)
普通 C (first C)14 KB67.325106.3
优化 C (new C)14 KB19.21617.5
Rust (rusqlite)321 KB45.93654.5
Rust (sqlite-loadable)298 KB40.53246.5

第一轮普通 C 版本在纯小数上跑出 106.3 纳秒的慢成绩,病根在于它直接调用了 C 标准库(glibc)的 strtod()。浮点数解析算法在近几年经历了重大演进,以 Eisel-Lemire 算法为代表的现代方案大幅降低了转换耗时。Rust 核心标准库早已并入了源自 fast-float 的现代 dec2flt 算法;而在 fast-float-rust 的历史基准测试中,canada 数据集上 C 标准库 strtod 耗时高达 286.10 纳秒,C++ fast_float 和 Rust fast-float 则分别只需 22.78 纳秒和 21.58 纳秒。

普通 C 扩展并不是输在了 C 语言的执行效率上。glibc 的 strtod() 背负着沉重的历史包袱,每次调用不仅要处理复杂的正确舍入边界,还要应对线程安全以及随时可能改变小数点符号的本地化(locale)检查。用几十年前的标准库接口去碰撞 Rust 内置的现代快速浮点算法,自然在起跑线上就被拉开了两倍身位。


AI 手搓的 17.5 纳秒快车道与被掩盖的框架税

当 Willison 要求 Claude 不依赖 strtod、自己写一个追平或击败 Rust 的解析函数时,AI 交出了一份把纯小数耗时从 106.3 纳秒直接砍到 17.5 纳秒的答卷。这个反超并非因为 Claude 现场发明了比 Eisel-Lemire 更强的通用浮点算法,而是因为它在工程完备性上走了捷径。

17.5 纳秒快车道剥离算法差距后,暴露出跨语言封装的胶水税(剖面示意)
17.5 纳秒快车道剥离算法差距后,暴露出跨语言封装的胶水税(剖面示意)
AI 优化版 C 扩展的解析路径与边界拆解 1. 字符扫描剥离 跳过 $ £ , % 累加十进制尾数 2A. 窄化快车道 (≤19ns) 尾数 ≤ 2^53 且小数位 ≤ 22 2B. 慢速兜底 (106ns+) 超界回退 strtod(含 locale 隐患) 3. 硬件浮点直除 需满足 FLT_EVAL_METHOD==0 注:基准测试中的常规金额与百分比均命中 2A 快车道,使解析耗时归零,凸显出 C 原生 API 的零封装优势。

优化版 C 扩展的源码原理非常直接,在扫描字符剥离符号的同时直接累加数字;当尾数不超过 2^53 且小数位数不超过 22 位时(还需编译环境满足 FLT_EVAL_METHOD == 0),因为 IEEE 754 双精度浮点数可以精确表示该范围内的整数与 10 的幂,代码直接用尾数除以查表得到的 10 的幂次;一旦超出这个范围,依然回退给慢速的 strtod()。

上半场测的是标准库算法代差,下半场测的是跨语言框架胶水税。

当解析本身的耗时被这条窄化快车道压缩到几纳秒后,C 版本(16 至 19 纳秒)与两个 Rust 版本(32 至 55 纳秒)之间剩下的 15 至 30 纳秒差距,暴露出的其实是 SQLite 扩展框架的封装损耗。尽管两个 Rust 扩展在编译时都开启了 LTO、单代码生成单元(one codegen unit)、panic 即中止(abort-on-panic)并剥离了符号,但每行数据触发 SQLite 标量函数回调时,Rust 封装库仍需跨越 FFI 边界,完成指针校验、上下文包装与字符串切片转换。sqlite-loadable-rs 官方的 hello world 基准测试早就印证了这一点,C 扩展体积仅 17 KB,而 Rust 扩展为 469 KB,且在空载调用上天然比 C 慢约 10% 至 15%。

审计 AI 生成的研究报告:给工程团队的三个现实参照

Willison 在遛狗途中仅凭几句自然语言指令,就让 Claude Opus 5.5 产出了完整的可编译跨语言代码、基准测试脚本,并自动克隆 GitHub 仓库打包成标准研究报告。这种研发效率对系统级程序员极具吸引力,但如果把这份完全由 AI 生成的报告当成生产环境选型依据,工程团队需要先看清三个被博客正文折叠的细节。

极致体积与零封装选 C,复杂逻辑用 20 纳秒胶水税换 Rust 安全
极致体积与零封装选 C,复杂逻辑用 20 纳秒胶水税换 Rust 安全
评测报告背后的三个关键数字还原 200万×4 非 800 万独立样本 4 种子 × 50 万输入 在 4 个扩展上重复比对 2 vCPU 共享云虚拟机环境 未披露具体 CPU 型号 纳秒级计时存在虚拟化噪声 14 KB C 扩展极致产物 对比 Rust 的 298–321 KB 零运行时与零胶水层负担

首先是测试口径与硬件噪声。原文提到的 800 万次模糊测试检查(fuzz checks),在仓库代码中实际是 4 个随机种子乘以 50 万个生成输入,共 200 万个独立测试用例,分别在 4 个扩展上运行并与 Python 的 float() 做按位比对(200 万 × 4 = 800 万次比对)。同时,整套测试运行在一台未披露具体 CPU 型号的 2 vCPU Linux x86-64 共享云虚拟机上(工具链为 GCC 13.3.0、Rust 1.97.0、SQLite 3.53.1),仓库文档自己也警告了共享云环境存在计时噪声。

更重要的是工业级参照系的缺失。如果开发者真的要在生产环境的 C 扩展里追求极致速度,既不该忍受 glibc strtod() 的缓慢,也不该让 AI 临时手搓一个遇到长小数就退回 strtod()(依然带有 locale 小数点解析隐患)的半成品。C 语言社区早就有 fast_float 的纯 C 头文件移植版 ffc.h;而 SQLite 源码内部也自带了专门的文本转浮点例程 sqlite3AtoF,其维护者还在 2026 年 3 月专门记录了 make fp-speed-test 编译目标,用于对比内部转换函数与 libc 的性能。

  • 风险.盲目将 AI 针对特定数据集优化的窄化算法搬入生产库,一旦遇到非标准 locale 环境(如将逗号视为小数点的欧洲语言环境)或超长精度浮点数,回退路径仍会触发解析不一致。

对于正在写 SQLite 扩展的数据库开发者来说,这道选择题的边界其实很清晰。如果是对二进制体积敏感的嵌入式分发,或者每秒要被调用上千万次的极简标量函数,用 C 语言配合 ffc.h 依然能拿到 14 KB 体积和零封装损耗的红利;如果扩展内部逻辑复杂、涉及大量字符串与内存操作,多花 20 纳秒的 FFI 胶水税换取 sqlite-loadable-rs 的内存安全与 Rust 标准库开箱即用的现代算法,依然是一笔划算的工程买卖。至于 rusqlite 和 sqlite-loadable-rs 的维护者,如何在百纳秒级的微操作里继续压榨出那 15 纳秒的边界转换开销,才是接下来真正值得较劲的硬骨头。