编译器是确定性的吗?

发布: (2026年2月22日 GMT+8 08:21)
9 分钟阅读

Source: Hacker News

2026年2月22日

Betteridge 说 “不”,对于普通的开发者体验来说,这个答案大体上是正确的。(另外,你完全正确!——我使用了 ChatGPT 来帮助我写这篇文章。)

我的看法

有两个互补的答案:

  1. 计算机科学答案 – 编译器是确定性的,作为其完整输入状态的函数。
  2. 工程答案 – 大多数实际构建控制完整的输入状态,因此输出随时间漂移。

正式模型

artifact = F(
    source,
    flags,
    compiler binary,
    linker + assembler,
    libc + runtime,
    env vars,
    filesystem view,
    locale + timezone,
    clock,
    kernel behavior,
    hardware / concurrency schedule
)

在实际操作中,大多数团队只保持 source(以及可能的 flags)不变,并将其他所有内容标记为“噪声”。正是这些“噪声”孕育了 non‑reproducibility

Source:

Ksplice 的经验教训

我在 2000 年代曾在 Ksplice 工作,那里我们在 RAM 中对运行中的 Linux 内核进行补丁,以便在不重启的情况下应用安全更新。
阅读 objdump 输出的易崩溃内核并不是日常工作,但出现的频率足以让 编译器输出源码意图 之间的差距不再是理论上的问题。

  • 我们通过 diff 旧的和新的编译对象,并将热补丁缝合到实时内存中,从而生成无重启的内核更新。
  • 大多数 diff 能够干净地映射到已更改的 C 代码。
  • 偶尔会出现“爆炸”情况,原因 与源码语义无关:寄存器分配不同、某些优化阶段行为改变,或段/布局的变动。
  • 相同的意图,却产生了不同的机器码。

具体的历史文物

GCC bug 18574 讨论了影响遍历顺序和 SSA 合并的 指针哈希不稳定性。该讨论展示了看似无害的工具链更改如何破坏可重复性。

关键区别

概念定义
确定性编译器给定完全相同的输入元组,总是产生相同的输出。
可复现构建两个独立的构建者能够重新创建位相同的产物。
可靠的工具链输出差异很少出现,出现时也很少影响功能正确性。

这些概念相关,但并不等价;理解它们的区别有助于对构建稳定性设定现实的期望。

编译器合约:语义,而非字节身份

评论者在这一点上是正确的:编译器应当保留 语义。对于具有已定义行为的程序,输出应在观察上等价于源语言的抽象机器。

这意味着指令顺序、寄存器选择、内联策略以及块布局都可以自由变动——只要外部可见行为保持不变。实际上,“可见行为”包括以下内容:

  • I/O 效果
  • 易失性访问
  • 原子同步保证
  • 已定义的返回值

……但 不是 字节对字节的指令相同。

重要注意事项

  • 未定义行为 会削弱或使语义保证失效。
  • 时序、微架构侧信道以及精确的内存布局 通常 超出核心语言合约 的范围。
  • 可重复构建 是比语义保留更严格的目标(它要求位相同,而不仅仅是行为相同)。

熵的来源

  • __DATE__, __TIME__, __TIMESTAMP__
  • DWARF/调试信息中的嵌入绝对路径
  • 构建路径泄漏(例如 /home/fragmede/projects/foo
  • 与区域设置相关的排序行为(LC_ALL
  • 文件系统遍历顺序
  • 并行构建和链接的竞争顺序
  • 归档成员顺序及元数据(ar, ranlib
  • 构建 ID、UUID、随机种子
  • 构建期间的网络获取
  • 工具链版本偏差
  • 主机内核 / C 库差异
  • 依赖不稳定指针或哈希遍历顺序的历史编译器内部实现

ASLR 注记: ASLR 并 直接对生成的二进制文件进行随机化;它随机化的是进程的内存布局。然而,如果编译器的某个阶段行为依赖于指针标识或顺序,ASLR 可能间接影响结果。

所以 “编译器是确定性的” 在理论意义上常常成立,但在实际操作中并不成立。

即使有可复现的制品,Ken Thompson 的 Reflections on Trusting Trust 仍然适用。

请记住,编译器并不是新技术:Grace Hopper 的 A‑0 system 可追溯到 1952 年的 UNIVAC I。(ChatGPT 只存在了约 4 年,而编译器已经有约 74 年历史。)

可复现构建:刻意工程

Debian 以及更广泛的 reproducible‑builds 运动(自 2013 年起)推动了这一主流理念:相同的源码 + 相同的构建指令应当生成位相同的产物

实践手册

  • 冻结工具链和依赖
  • 使用稳定的环境 – 例如 TZ=UTCLC_ALL=C
  • 设置 SOURCE_DATE_EPOCH 为固定时间戳
  • 规范化 / 去除易变的元数据(时间戳、ID 等)
  • 统一路径前缀
    -ffile-prefix-map=
    =
    -fdebug-prefix-map=
    =
  • 创建确定性的归档文件 – 例如 ar -D
  • 从构建图中移除网络访问
  • 在密封容器或沙箱中构建
  • 在 CI 中持续对比不同构建者的产物

结果

  • 可重复 – 你可以运行相同的命令并得到相同的结果。
  • 可复现 – 不同机器生成的二进制文件完全相同。
  • 可验证 – 任何人都可以检查输出是否与源码匹配。
  • 密封 – 构建过程与外部状态隔离。
  • 确定性 – 没有隐藏的随机性影响结果。

当前状态

我们现在拥有这些了吗?
在许多生态系统中答案是 大多数情况下是,但这需要多年在编译器、链接器、打包工具和构建系统之间进行有意的工作。我们是通过逐步攻克各种棘手的边缘案例而达到的,而不是仅仅挥挥手就宣称纯净。

为什么这对 LLM 很重要

这个问题常常被提出:“如果 LLM 是非确定性的,vibecoding 还能算理性吗?”
在回答之前,先决定你想要的是 计算机科学 视角还是 工程 视角。

与停机问题的类比

  • 我们在形式、理论意义上并未解决停机问题。
  • 但在实践中,LLM 能够发现一个损坏的 for‑loop,解释条件错误的原因,甚至提出修复方案。

工程现实

工程从不依赖 完全确定性的 智能。
相反,它依赖于:

  1. 受控的接口
  2. 测试预言机
  3. 可复现的流水线
  4. 可观测性

我已经“被 AI 药丸”灌得够多,能够每天驾驶一辆 comma.ai 的汽车,但我仍然要求对任何生成的代码设置确定性的验证关卡。
我的女友更喜欢汽车更平稳、波动更小的表现——这提醒我们,概率系统 仍然可以提供 更好的运营结果

模式 for L

0 浏览
Back to Blog

相关文章

阅读更多 »