目录
2 分钟阅读 2026

LLM 写了 57 万行 Rust 代码,编译通过,却比 SQLite 慢了 20171 倍

有人对一个完全由 LLM 生成的 SQLite Rust 重写版本做了性能基准测试。能跑通、能编译、看起来没问题的代码,和真正正确的代码之间,差距竟然达到五个数量级。

有人用 LLM 完整重写了一个 SQLite,用 Rust 写的,然后跑了性能基准测试。代码能编译,测试也过了,写法干净、结构清晰、符合 Rust 惯用风格。但在一个最基础的主键查询上,它比 SQLite 慢了 20171 倍。

这个数字让我停下来想了很久。不是因为 LLM 生成的代码跑得慢有什么稀奇,而是因为慢的原因本身。这段代码没有任何编译器或测试套件能发现的错误。B-tree 实现是正确的,query planner 存在,存储引擎也工作正常,每一个模块单独看都站得住脚。但整个系统放在一起,几乎无法使用。

我花时间仔细读了基准测试分析和源码。发现的这些模式在 LLM 生成的项目里反复出现,我认为它们指向了这些模型写代码方式的某个根本性问题。

B-tree 在那里,但 query planner 从来不调用它

在 SQLite 里,一个 PRIMARY KEY 查询会走 B-tree 路径,时间复杂度是 O(log n)。where.c 里四行代码检查 iPKey,然后把查询直接路由到树上。这是一个只有在你理解整个系统如何协作时才说得通的微优化。

LLM 生成的版本也有 B-tree 实现,单独跑也完全正确。问题在于 query planner 在主键查询时从来不调用它。is_rowid_ref() 函数只认三个字面量字符串:“rowid”、“rowid” 和 “oid”。如果你把某列声明为 id INTEGER PRIMARY KEY,planner 不会把它识别为 rowid 的别名,于是每次查询都退化成全表扫描。

算一下就知道这有多惨。100 行数据查 100 次,B-tree 路径大约需要 700 次比较,全表扫描路径超过 10000 次。但真正的伤害来自算法复杂度的变化:每次查询从 O(log n) 变成 O(n),在完整的基准测试套件里叠加下来,就堆出了那 20171 倍的差距。

这种 bug,除非你专门写一个性能基准,否则没有任何单元测试能抓到它。B-tree 正常,扫描也正常,planner 选错了路,所有测试全部通过。

防御性默认值像利滚利一样叠加

这个案例比单纯的路由 bug 更有意思的地方在于:即使修掉 query planner 的问题,这个重写版本还是大约慢了 2900 倍。剩下的差距来自一连串单独看都合理的决策。

每次执行查询都会 clone 完整的 AST,并重新编译成 bytecode。SQLite 复用 prepared statement 句柄。两种做法都说得通,但在高频场景下,每次执行都 clone 一个 AST 代价极高。

每次读取页面都会在堆上分配一个新的 4KB buffer。SQLite 的 page cache 直接返回已加载内存的指针。LLM 选择了安全、直接的路径:分配、读取、返回。它能跑,只是在每次查询要读成千上万个页面时,慢了好几个数量级。

每次 commit 都从头重建整个 schema。SQLite 比较一个整数 cookie 值,如果 cookie 没变,schema 就还有效。重写版本没有这个概念,所以每次都做了全量工作。

每条语句都触发 sync_all() 来把所有文件元数据刷到磁盘。SQLite 用的是 fdatasync(),只刷文件数据,跳过元数据同步。在写密集的场景下,这个差别非常显著。

我把这个现象叫做防御性默认值的复利效应。每个选择单独看都有合理的理由:clone AST 避免了 Rust 的所有权复杂性,分配新 buffer 防止 use-after-free,每次重建 schema 避免缓存过期,调用 sync_all() 提供最强的持久化保证。

但性能损耗是相乘的,不是相加的。四个 10 倍的惩罚叠在一起,结果不是慢 40 倍,而是慢 10000 倍。LLM 不会对这种叠加效应做推理,因为它是相对孤立地生成每个函数的。它在局部做了优化,却在全局付出了代价。

用 8.2 万行代码替换一行 cron 命令

同一个开发者的另一个 LLM 生成项目,用另一种方式展现了同样的模式。问题是:Rust 的 target/ 目录里的构建产物会随时间积累,吃掉磁盘空间。LLM 的解决方案是一个 8.2 万行的 Rust daemon,带七个 dashboard,还有一个贝叶斯评分引擎来决定清理哪些产物。

现成的解决方案是 find ./target -type f -atime +30 -delete,一行 cron job,零依赖。或者用 cargo-sweep,一个已经存在的官方社区工具,还处理了那个 daemon 没覆盖到的边缘情况。

LLM 生成的项目拉了 192 个依赖。作为对比,ripgrep 作为 Rust 生态里最精密的搜索工具之一,用了 61 个。

这是我反复看到的一个模式:LLM 构建你要求的东西,而不是你需要的东西。如果你的 prompt 是”构建一个能智能管理 Rust 构建产物、带监控和评分的系统”,你得到的就是这个。模型没有机制退后一步问问:这个问题到底需不需要一个系统?它不知道 target/ 目录大小是 Rust 社区多年来的老问题,已有成熟解法。它也不会考虑 192 个依赖对比零个依赖的维护成本。

研究结论指向同一个方向

我很想知道这两个项目是不是个例,于是去看了更广泛的研究数据。它们不是。

METR 做了一个随机对照试验,参与者是 16 名有经验的开源开发者。使用 AI 工具的那组完成任务的速度比对照组慢了 19%。让我印象最深的一点是:实验结束后,AI 组的开发者认为自己快了 20%。主观感受到的生产力提升,和实际测量结果完全相反。

GitClear 分析了 2.1 亿行代码,发现粘贴复用的代码首次超过了重构改进的代码。这个趋势和 AI 编程工具的采用率直接相关。代码被添加的速度,快过了它被改进的速度。

Google 的 DORA 2024 报告发现,AI 采用率提升 25%,与部署稳定性下降 7.2% 相关。更多 AI 生成的代码进入生产,更多故障随之而来。

NeurIPS 2024 的 Mercury 基准测试在标准编程测试里加入了效率指标。当你衡量的不只是”能不能产出正确输出”,而是”能不能在不浪费资源的情况下产出正确输出”时,通过率跌到了 50% 以下。

这些都不意味着 LLM 在写代码上没有用,我自己也一直在用。但它确实说明”能编译、测试能过”是一条危险的低标准线。从看起来可信的代码到真正正确的代码之间,才是真正的工程工作发生的地方。

这对开发者的真正要求是什么

核心问题不是 LLM 写出了坏代码,而是它写出了局部连贯、整体不连贯的代码。每个函数单独看都说得通,但整个系统不行。这正是传统测试会漏掉的失效模式,因为测试验证的是局部行为。

需要的是能覆盖这些盲区的评估手段:基准测试而不仅是单元测试,CI 里的性能预算而不仅是正确性检查,在验证模块能不能用之前先做架构审查问”这个模块为什么要存在”,做依赖审计时把方案的复杂度和问题本身的复杂度做对比。

问题不是”这段代码看起来对不对”,而是”我们怎么证明它是对的”。证明这件事需要的是系统级的思考能力,而这正是 LLM 目前所缺乏的。

你要求的东西和生产环境真正需要的东西之间的差距,就是工程判断力存在的地方。没有测量,代码生成不过是 token 生成。

订阅通讯

获取关于我最新项目、文章以及 AI 和 Web 开发实验的更新。