重回 C 语言
高级语言用机器的时间换人的时间;编程智能体改变了这个价码
四十年来,编程语言的历史大体是一部用机器时间换人的时间的历史。垃圾回收、动态类型、丰富的标准库、虚拟机、层层叠叠的框架:每一样都让程序更好写,也让它跑得慢一点。Python 正是凭着这笔交易赢下了这十年。C 只给你硬件,几乎别无其他,它守住了内核和数据库,丢掉了其余的大部分。
这笔交易在当时是对的。我认为它的时代正在结束。当智能体来写代码,高级语言当初要省下的人力成本基本消失了,而它们收取的机器成本并没有消失。C 回来了,至少,支持它的理由回来了。
每一层抽象都是一笔交易
语言变复杂并非偶然。每一种便利,都把一份负担从程序员身上挪进运行时。有了垃圾回收,你不必决定内存何时死去;运行时稍后替你决定,并为此停顿。有了泛型容器,你不必写哈希表;每次查找都要为间接寻址付费。有了虚拟机,你不必考虑处理器;JIT 在运行时替你考虑,前提是你先熬过它的预热。
换个说法:高级语言推迟决定。它们让程序员不必写明内存布局、生命周期和类型,而把这些决定推到运行时再绑定(见《每个决策都有它的绑定时间》)。推迟要花机器周期,但把每个决定都写下来要花人,而人才是稀缺资源。C 是相反的设计:简单,贴近硬件,每一个决定都由写代码的人明确作出。危险,而且快得惊人。
写下来,如今很便宜
编程智能体同时改变了这笔交易的两端。让 C 显得繁琐的样板代码,哈希表、可增长数组、手动内存管理,对智能体来说只是几秒钟的事。它也不需要靠抽象才能把一个大程序装进脑子里。于是推迟决定的主要理由消失了;你又负担得起早绑定了。
C 的另一项成本是缺陷,这里我想说得精确一些。智能体仍然会写出缺陷,包括 C 臭名昭著的那类内存缺陷。变化在于,智能体还会不厌其烦、无需提醒地运行那些能抓住缺陷的工具:AddressSanitizer、UndefinedBehaviorSanitizer、差分模糊测试、暴力枚举的判定器。人类知道这些工具存在,却大多跳过它们,因为搭起来又慢又无聊。C 的危险从来不只是缺陷容易写出来,更在于检查很昂贵,而没有人检查得够多。
一个实验:用 C 重写 Boomslang
为了检验这一点,我让一个编程智能体移植我的一个研究系统。Boomslang 检查一个数据库是否兑现了它承诺的隔离级别。它读取客户端发出与收到的内容的轨迹,搜索能解释这条轨迹的事务顺序。这个问题在一般情况下是 NP 完全的,所以这个检查器其实是一个小编译器:解析八种格式的轨迹,构建一张记录已知与未知关系的图,剪枝,再把剩下的交给一个带图无环理论的 SAT 求解器。它发表时的配套实现约有 24,000 行 Java,外加通过 JNI 调用的 MonoSAT,一个 98,000 行的 C++ 求解器。
移植只用了两条提示:“用 C 语言实现 Boomslang”,以及一个循环,让智能体一直干下去,直到 C 版本能跑 Java 版本的同一批测试。二十二小时后,中间我一条消息都没发,它做到了。又过了几天、几条简短的后续提示,在配套实现自己的实验上,数字是这样的:
- 快 60 倍(在两者都能跑完的 78 次运行上取几何平均),内存少用 48 倍。在 50,000 个事务的 TPC-C 上,Java 用 173 秒、62 GB;C 用 1.2 秒、0.63 GB。
- 第一次完整提交时为 4,900 行 C,对比 23,900 行 Java 和那个 98,000 行的求解器。除了 libc 和 pthreads,没有任何依赖。
- 除一次运行外,判定结果与 Java 一致。
完整的过程,连同每一条提示,见移植笔记(英文)。
速度从哪里来
同时换语言又换算法的移植,很难分清功劳归谁,所以 C 版本有一个开关,可以构建与 Java 完全相同的图。在同一张图上,C 大约快 30 倍。这是语言的功劳:没有 JVM 启动,没有垃圾回收,没有 JNI 边界,数据按算法读取的方式摆放。剩下的 2 倍(大规模运行上是 4.5 倍)来自算法上的改动,是智能体在有了快速的底座和可供对照的参考之后才去尝试的,比如一种可达性结构,其内存随“事务数乘以会话数”增长,而不是随事务数的平方增长。
求解器是更有意思的数字。MonoSAT 是一个带多种图理论的通用 SMT 求解器。Boomslang 只需要其中一种:无环性。智能体为这一种理论写了一个求解器:887 行。通用库之所以存在,很大程度上是因为写专用版本对人来说太贵。当它不再昂贵,你就写那 887 行,只做你需要的事。
前提是有裁判
如果这些 C 代码无法被信任,以上一切就没什么意义,而这正是我认为最重要的部分。每一个关于正确性的论断,都建立在与某个独立对象的对照之上:在数千条随机轨迹上对照 Java 实现,在另外 40,000 条上对照一个暴力枚举的可串行化判定器,对 Adya 论文中定义的每一个隔离级别对照其字面解读。这些裁判大多是智能体自己搭的,而且是在需要它们之前就搭好了。
裁判还找出了智能体自己最严重的缺陷:一个从第一次提交起就存在的求解器错误,在任何测试中都没有产生过错误判定,一天之后才在 AddressSanitizer 下以堆溢出的形式浮现。这正是人们害怕 C 的那种缺陷,也正是 C 的缺陷本该被抓住的那种方式。
智能体从来不必一次就做对。它必须可被检查。
C 仍然要付出什么
让我承认那些我并不笃定的部分。同样的论证也支持 Rust,它在编译期就能抓住消毒器在运行期才抓住的问题;“C 回来了”在一定程度上是关于底层语言整体的论断,C 只是一个有代表性的例子。C 求解器在一个刻意构造的困难实例上仍然比 MonoSAT 慢,因为它的分支启发式更简单。
所以 C 剩下的成本是审阅,而审阅 5,000 行,比审阅 24,000 行再加一个依赖容易得多。
说得准确些:回来的不是对 C 的怀旧,而是那条老规矩:最清楚程序需要什么的人,应当把它写下来;如今,写下来已不再耗费人力。
参考文献
- Jian Zhang, Shuai Mu, and Cheng Tan. Making Transaction Isolation Checking Practical. ACM ATC 2026.
- Atul Adya. Weak Consistency: A Generalized Theory and Optimistic Implementations for Distributed Transactions. PhD thesis, MIT, 1999.
- Sam Bayless, Noah Bayless, Holger H. Hoos, and Alan J. Hu. SAT Modulo Monotonic Theories. AAAI 2015.