编译器是缓存起来的推理

如果超级智能可以直接编写机器码,我们还会发明编程语言吗?把这个问题追问到底,会发现编程语言从一开始究竟是为了什么。

作者
Paco Xu
发布日期
阅读时长
约 12 分钟

I. 异端之论

科技圈里流传着一种观点,乍一听,像是有人连听了太多场主题演讲之后才会说出口的话。它大致是这样的:编程语言只是一种临时安排。Python、Java、C——全都是脚手架,是为人类搭起来的;人类需要 while 和 return 这样的词,因为他们无法用电压来思考。这种观点认为,一旦人工智能足够强大,它就会拆掉这些脚手架:跳过源代码,跳过编译器,直接写出二进制——处理器真正“吃”进去的那些原始的 0 和 1。

我的第一反应是不屑一顾。随后我意识到,“听起来不对”并不等于“确实不对”,而前者往往只是熟悉的世界受到威胁时的一种感觉。于是,我做了一件如今已成为我略显古怪的习惯的事:和一个 AI 争论这个问题。

和我一样,这台机器也从教科书式的反驳开始。编译器是确定性的,语言模型是概率性的。把同一份源代码交给编译器两次,你会得到两份相同的二进制;把同一个提示词交给模型两次,你可能得到两个不同的程序。源代码可以被审查、比对和审计,而神经网络吐出的一团二进制,几乎是最不透明的产物。四十年前,Ken Thompson 在图灵奖演讲《反思对信任的信任》(Reflections on Trusting Trust)中警告:对于不是自己亲手写下的代码,我们永远无法完全信任,即便它经过了编译器也不行。一个无法审计的 AI 编译器,将是他的噩梦在全球尺度上的重演。

这些都没错。但随着我们层层拆解,最著名的那条反对意见——确定性——反而显得没有看上去那么有力。语言模型是可以做到确定性的。固定随机种子,把温度设为零,再从工程上消除 GPU 内部的浮点噪声,同一个提示词每次都会产出相同的字节。可这样得到的,是一台确定性地出错的机器:每一次运行,它都会交给你一模一样的隐蔽 bug。

我们真正在意的,从来都不是确定性。它只是另外两种更深层属性的替身。第一种是语义保持。编译器的设计目的,是把程序的含义完整地从一种表示形式带到另一种表示形式。它遵守一份契约——语言规范——一旦违约,我们就称之为 bug,并加以修复。(有些研究者走得更远,构建了 CompCert:一个正确性经过数学证明的 C 编译器。)

第二种是稳定性。修改一行源代码,大致来说,二进制中也只有一个区域随之改变。这种局部性是看不见的基础设施。正是它,让增量构建、缓存、可复现构建,以及 git bisect 那种耐心的“考古”成为可能。而一个直接编写二进制的模型,可能仅仅因为指令里改了一个词,就把整个程序重新洗牌。

所以,这条反对意见的诚实版本并不是“AI 是随机的”,而是:编译器是稳定的、保持语义的函数,而 AI 在默认情况下两者都不是。

这是一条有力的反对意见。但它针对的是今天这个样子的 AI。我想知道的是,它能否构成对这个想法本身的反驳。于是,我们做了一个实验。

II. 1955 年的精灵

想象现在是 1955 年。世界上最重要的计算机之一是 IBM 704,一台占满整个房间、靠穿孔卡片输入的机器。那时还没有现代意义上的编程语言,只有机器码,以及汇编——覆盖在机器码之上的一层薄薄的助记符。现在,往这个世界里放进一个不属于那个时代的存在:一个超级智能,它能听懂你想要什么,并写出完美无缺的机器码来实现它。

还会有人发明 FORTRAN 吗?

要回答这个问题,就得记起 FORTRAN 当初为什么被发明。它并非诞生于审美,而是诞生于预算。John Backus 在 IBM 提出它的时候,面对的问题是:程序员已经成了计算中最昂贵的部分。Backus 后来回忆,编程和调试的成本可能占到运行一台计算机总成本的四分之三。机器很快;人却很慢,容易出错,还健忘。

如今让我印象深刻的,是这个想法当年遭遇了多大的阻力。早期的编译器被称为“自动编程”,许多专家怀疑机器写出的代码永远比不上熟练的人类。Grace Hopper 的 A-0 系统是最早的编译器之一,她后来回忆,曾有人告诉她:计算机只能做算术,不能写程序。Backus 的团队之所以对优化近乎执念,正是因为他们知道,任何比手写汇编慢的东西都会被程序员拒绝。如果这种怀疑听起来似曾相识,那就对了。我们正在把同样的争论再进行一遍,只是高了一层楼。

FORTRAN 之后的每一种语言,归根结底,都是为人类某种局限安装的假肢。命名变量之所以存在,是因为我们记不住某个内存地址里存的是什么。循环和函数之所以存在,是因为几十条指令之后我们就会理不清头绪。类型系统之所以存在,是因为我们会犯自己看不见的错误。可读的语法之所以存在,是因为程序员以团队协作,必须彼此理解。可移植的语言之所以存在,是因为为每一台新机器重写程序,在人力上代价高得惊人。

现在,把这些工作统统交给精灵。它的工作记忆远不止寥寥几项。它不会打错字。它不需要同事来读它的代码,因为它根本没有同事。催生了 FORTRAN、随后是 C、再到 Java 和 Python 的每一种压力,都就此烟消云散。我猜,在精灵的时间线里,我们所熟知的那条编程语言谱系根本不会开始。

影响并不止于软件。我们的硬件,同样是被人类程序员及其编译器塑造的。20 世纪 70 年代末,John Cocke 带领的 IBM 研究团队注意到,编译器只用到了处理器所提供的那些繁复指令中的一小部分。这一观察推动了 RISC 架构的诞生,而今天几乎每一部手机里都有它的身影。几十年后,英特尔的 Itanium 押注于一种设计:它需要编译器以近乎未卜先知的远见来调度指令;编译器始终没能真正做到,这款芯片也成了业界代价最高昂的失败之一。索尼用于 PlayStation 3 的 Cell 处理器是一项工程奇迹,同时也是出了名让开发者头疼的存在。

在精灵的时间线里,这些失败都会变成胜利。如果程序员是超级智能,“难以编程”就不再是设计约束。芯片设计者可以只为原始速度和效率而优化:暴露的流水线、手动管理的内存、奇特的数据流机器。

再往前推一步,就连指令集——那份自 1978 年起让 x86 延续至今、硬件与软件之间的神圣契约——也开始消解。向后兼容之所以存在,是因为重写软件代价高昂。如果为新芯片重新生成每一个程序几乎不花成本,那么每一代芯片都可以说它自己的语言。最终,精灵或许会把芯片和程序当作同一个对象,一并设计出来。

软件本身的质地也会改变。今天,我们维护程序:打补丁、重构,在几十年间不断累积技术债。在精灵的时间线里,软件不是被维护的,而是被重新推导的。你不去修它。你重新陈述你想要什么,一个新的程序就出现了。

思想实验进行到这里,那个大胆的想法看起来不只是可信,简直就是正确的。然后,精灵开始惹麻烦了。

III. 精灵解决不了的事

第一个麻烦,是民间故事里最古老的那一个:精灵给你的,是你说出口的,而不是你真正想要的。

想象 1962 年的一家银行,它的工资系统由超级智能根据一位经理的口头描述编写而成。某一天,系统不折不扣地执行了经理说的话,却完全不是他本来的意思。于是有了一场诉讼。律师们争论一句话究竟是什么意思,工程师们也一样。慢慢地、痛苦地,整个社会意识到:它需要一种陈述意图的方式,而且这种方式只允许唯一一种解读。

换句话说:它发明了一种语言。不是一种告诉机器如何做事的语言,而是一种陈述什么必须完成、什么绝不能发生的语言。我们自己的时间线里也有这样的语言——像 TLA+ 这样的规约语言,像 Lean 这样的证明助手——但它们处在这个行业的边缘,主要由数学家,以及那些构建“绝不允许失败”的系统的工程师使用。我敢打赌,在精灵的时间线里,它们会比任何类似 C 的东西早几十年出现,并成为大多数人一生中唯一会做的编程。这门手艺将从“指挥机器”转向“精确地表达愿望”。程序员会变得更像立法者。

第二个麻烦是信任。假设精灵为 20 世纪 60 年代的一套导弹防御系统编写控制软件。没有人能读懂它,也没有人能审视精灵是如何思考的。凭什么有人应该相信,这份二进制确实做到了规约所要求的事?

一种选择是干脆信任精灵——这其实就是换上了时代戏服的 AI 对齐问题。另一种选择是要求证明。而在这一点上,我们的时间线同样已经勾勒出了答案。20 世纪 90 年代末,计算机科学家 George Necula 和 Peter Lee 提出了所谓的“携带证明的代码”(proof-carrying code):程序在交付时附带一份数学证明,证明它遵守某项给定的策略,而这份证明可以由一个小巧、简单的验证器来检查。

不可信 · 每次运行都可能不同
  1. 01规约什么必须完成,什么绝不能发生
  2. 02精灵陌生、不可读、非确定
  3. 03二进制 + 证明产物自带证据
可信 · 小到可以逐行阅读
  1. 04证明检查器接受或拒绝
图 1 携带证明的代码把信任从过程转移到了产物上。唯一需要信任的,只有检查器。

这种安排的精妙之处,在于它把什么移出了信任圈。你不再需要信任编译器。你不再需要信任精灵。你只需要信任证明检查器——一段小到人可以逐行阅读的软件,就像审阅一部宪法那样。从顶端的规约到底端的二进制,中间的一切都可以是陌生的、不可读的,甚至可以出自一个每次都做出不同选择的过程。只要每一份二进制都附带有效的证明,随机性就不再重要。精灵可以交给你一千个不同的程序,而每一个都可以被证明是正确的。

注意发生了什么。我们最初的那条反对意见——AI 是随机的,编译器是确定性的——与其说被驳倒了,不如说变得无关紧要了。信任,已经从过程转移到了产物上。

IV. 缓存起来的推理

这个故事还有最后一个转折,本文的标题正来自于它。

假设精灵已经工作了十年。它写了数以百万计的程序,其中许多彼此相似:给这些记录排序,解析这个文件,绘制这个窗口,把钱从一个账户转到另一个账户。每一次,它都从第一性原理出发,一路推理到电压层面。

即便对超级智能而言,这也是一种浪费。思考是有成本的——时间、能量、算力。任何一再解决同类问题的智能体,最终都会做同一件事:造一个工具。一个聪明人如果总在手工重复某项繁琐的任务,就会写一个脚本。脚本是对已完成思考的记录,几乎可以零成本地反复使用。

所以,任由精灵自行发展,它会为自己构建编译器。不是因为它需要可读的代码,而是因为推理昂贵,缓存便宜。它会发展出中间表示、翻译规则,以及经过验证的模式库,全都为它自己的目的而调校,对我们来说很可能难以理解。面对全新的问题、一次性的任务,或每一纳秒都至关重要的程序,它仍会直接编写机器码。而对于一切例行公事,它会伸手去取它的缓存。

这重新定义了编译器是什么。我们习惯把编译器看作翻译器,一种把一种语言转换成另一种语言的机器。而编译器更应该被理解为缓存起来的推理:几十年来,人类关于如何把意图转化为高效指令的思考,被冻结成一个过程,只需付出电费,就能运行十亿次。现代编译器里的每一个优化遍(pass),都是某位工程师曾经有过的一个洞见——为的是从此再也没有人需要重新想一遍。

我认为,“AI 会直接输出二进制”这个想法错就错在这里——错不在它的野心,而在它的经济账。AI 研究者 Rich Sutton 在他影响深远的文章《苦涩的教训》(The Bitter Lesson)中指出,纵观这个领域的历史,由算力驱动的通用方法一次又一次击败人类精心设计的巧思。人们很容易把编程语言和编译器看作正是这条教训所预言、终将被一扫而空的那种巧思。

但看看《苦涩的教训》真正扫除了什么:手工设计的视觉特征、基于规则的语音处理流程、专家编写的国际象棋启发式规则。在每一个案例中,人类构建的东西都是对某种杂乱事物的近似,而学习能把它近似得更好。编译器并不是一种近似。它是一种精确的变换,几毫秒就能完成。用一个近似且昂贵的过程,去替换一个精确且廉价的过程,这恰恰违背了《苦涩的教训》,而不是顺应它。

这和现代 AI 模型在被要求计算两个大数相乘时,会去调用计算器、而不是逐位预测数字,是同一个道理。它当然可以预测。只是毫无益处。

更深层的一点是:推理与缓存并非对手。智能产生洞见;缓存让洞见变得廉价。一个充满超级智能的世界,编译器不会更少。它可能会多得多,而其中大多数由机器编写,为机器服务。

V. 语言栖居于边界

退后一步看,整个思想实验可以归结为一条原则:当两方必须就某件事达成一致、却又无法共享同一个心智时,就需要语言。

在单一心智之内,不需要语言;思考自然发生。而在心智之间,语言不可避免。编程语言曾坐落在两条这样的边界上:人与机器之间,以及人与人之间——那些必须阅读并扩展彼此工作的团队。把人类从实现的劳动中移除,这些特定的语言就会坍塌。但语言并不会消失。它会迁移到剩下的每一条边界上。

边界
留存下来的语言
人 与 超级智能
规约
超级智能 与 验证器
证明
程序 与 程序
协议、文件格式、接口
超级智能 与 超级智能
共享表示
图 2 当人类退出实现环节,语言并不会消失,而是迁移到剩下的边界上。

在人与超级智能之间,它成为规约:对我们想要什么的精确陈述。在超级智能与验证器之间,它成为证明。在程序与程序之间,它成为协议、文件格式、接口,因为独立构建的系统仍然必须就一条消息的含义达成一致。而在一个超级智能与另一个超级智能之间——它们由不同的公司打造,或诞生于不同的年代——也必须有某种共享的表示。即便是神,也需要条约。

那么,那个大胆的想法是对的吗?部分是对的,而且对的方式比它的拥护者通常所指的更深。倒回 1955 年,再加入一个超级智能,我相信大多数软件都会以机器码的形式、由机器写成。C、Java 和 Python 的时代根本不会发生。这个想法在实现语言上是对的。它越界的地方,在于想象语言本身会消失。语言只是移动了:向上进入规约,向旁进入证明与协议,向内进入机器自己的私有编译器。

这把我们带回当下,也引出一个令人不太舒服的推论。我们的编程语言——我们教授的、争论的、赖以建立职业生涯的那些语言——在某种程度上,或许是人类曾是瓶颈的那个时代留下的化石。它们就像中世纪缮写室里的那些惯例:缩写、画好格线的页边、一丝不苟的笔迹。当每一本书都要靠人手抄写时,这些惯例合情合理;而在印刷术让它们变得多余之后,它们又延续了很久。

我们不会很快摆脱它们。我们背负着几十年的遗留代码、工具链、制度和习惯,而 1955 年的那条时间线从来不必背负这些。但如果这个思想实验指向某处,它指向的是这里:在那样的未来里,人类对软件的贡献不是指令,而是意图,并且被陈述得足够精确,可以被证明;信任建立在小巧、可读的检查器之上,而不是建立在我们读懂一切的能力之上;而机器,无论它们还会变成什么,都会继续为自己打造工具——因为即便是最强大的心智,也宁愿记住一个答案,而不是把它推导两遍。

在那样的世界里,编译器并不会消亡。它终将显露出它一直以来的本来面目:不是翻译器,而是被保存下来、留待日后使用的思考。