盖一栋楼,需要把建筑师的意图(设计图)翻译成施工图,再翻译成具体的砌砖指令。打一场官司,需要把当事人的诉求翻译成法律主张,再翻译成庭审陈述。编程也是一样——源码是人类意图的形式化表达,编译与执行是意图一步步固化为物理动作的全过程。
这个过程的核心问题不是”怎么翻译”,而是:意图如何在不同的抽象层次上保持一致,最终成为 CPU 可以执行的指令?
一、程序是什么?(本体论)
一个程序同时存在于多个层次:
程序员写的文本 → 语法结构(AST) → 带类型的含义(IR) → 机器指令 → 可观测行为这不只是格式的变化,每一层都在回答一个不同的问题:哪个层次才是”真正的程序”?
不同语言社区给出了不同的答案:
- C:程序是机器码,源码只是书写工具。C 标准定义了可观测行为,但允许编译器以任何方式实现——未定义行为(UB)是因为”你的意图”不够精确
- JVM 语言(Java / Kotlin / Scala):程序是字节码,JVM 规范是合同。“一次编译,到处运行”的承诺绑定在字节码层,而非源码层
- Python:程序是源码,其他都是实现细节。CPython、PyPy、Jython 可以有完全不同的内部机制,只要可观测行为一致
- Haskell:程序是数学含义(指称语义),执行是附属品。GHC 可以彻底重写你的代码,只要输出相同
关键洞察:编译不是翻译,是判决。 翻译只改变形式,可以保留歧义。编译必须消除歧义——a + b 在源码里可以是整数加法、字符串拼接、自定义运算符重载,编译过程必须选定一个,不能保留含糊。每一次编译,编译器都在回答无数个源码没有明确回答的问题。
二、意义如何渐进固化(语义论)
编译管线的本质,不是把源码换成另一种格式,而是逐层收窄解释空间:
源码 → 最大歧义(语法上允许多种解释)
↓ 词法/语法分析
AST → 结构确定,语义未定
↓ 语义分析(类型检查、名字解析)
IR → 意义固定,执行方式未定
↓ 优化(消除冗余抽象)
机器码 → 完全承诺,无歧义,无抽象graph LR A["源码"] --> B["词法分析<br/>Lexer"] B --> C["Token 流"] C --> D["语法分析<br/>Parser"] D --> E["AST"] E --> F["语义分析<br/>类型检查 / 名字绑定"] F --> G["带类型的 IR"] G --> H["优化<br/>消除冗余抽象"] H --> I["代码生成"] I --> J["目标代码"] style A fill:#4dabf7,color:#fff style J fill:#69db7c,color:#fff style H fill:#ffa94d,color:#fff
词法与语法:识别结构
词法分析(Lexer)把字符流切分为 token,语法分析(Parser)把 token 组成 AST。这两步只处理形式——if (x > 0) { ... } 是什么结构,和它意味着什么无关。
关键算法:有限状态自动机(词法)、递归下降 / LR(k)(语法)。
语义分析:固化意义
语义分析是编译器真正”理解”程序的阶段:类型检查(这个操作合法吗?)、名字解析(这个 x 指的是哪个 x?)、作用域分析(这个变量在哪里有效?)。
这一步之后,歧义消失——每个名字有确定的绑定,每个操作有确定的类型含义。Hindley-Milner 类型推导算法在这里完成大部分工作(见 01 类型系统)。
中间表示:分离关注点
为什么需要 IR?它是前端(懂程序员语言)和后端(懂机器语言)之间的公共语言,分离了关注点。
SSA(Static Single Assignment):每个变量只赋值一次,用 φ 函数在控制流汇合点合并不同路径的值。这个约束极大简化了数据流分析和优化算法。几乎所有现代编译器优化(死代码消除、常量折叠、内联)都基于 SSA 形式。
LLVM IR:Rust、C/C++、Swift、Zig 共享的后端。LLVM 的设计哲学是前后端分离——各语言实现自己的前端,共享 LLVM 的优化和代码生成。Clang、rustc、swiftc 都是 LLVM 的前端。
字节码:虚拟机的指令集,比机器码高级,比源码低级。JVM 字节码跨平台的承诺绑定在这一层;Python .pyc 是编译后的 CPython 字节码——Python 是编译的,只是这个步骤对用户透明。
优化:消除冗余抽象
优化的哲学前提:在不改变可观测行为的前提下,编译器可以任意变换程序内部结构。
常见优化技术:
- 常量折叠(Constant Folding):编译时计算
2 * 3 * PI,运行时直接用结果 - 内联(Inlining):把函数调用展开为函数体,消除调用开销,也让后续优化看到更多上下文
- 死代码消除(DCE):删除不可达代码或结果未被使用的计算
- 逃逸分析(Escape Analysis):判断对象是否”逃出”当前作用域,未逃逸则可以栈分配,省去 GC 压力
- 循环向量化:把标量循环变成 SIMD 指令,单指令处理多个数据
Rust 能做激进优化的原因:类型系统保证了”无别名(no aliasing)“——编译器知道两个引用不会指向同一块内存,可以大胆重排指令。C/C++ 没有这个保证(除非用 restrict),因此相同的代码 Rust 编译出的性能有时优于 C。
三、何时固化意义?(时态论)
管线描述了”怎么固化”,时态论回答”什么时候固化”。这是 AOT / JIT / 解释的本质区别,代表三种截然不同的时间哲学:
graph TD A["源码"] --> B{"何时固化意义?"} B --> C["AOT<br/>执行前全部决定"] B --> D["JIT<br/>执行中动态决定"] B --> E["解释<br/>每步现场决定"] C --> C1["先知——静态分析预测未来<br/>Rust / C / Go / Swift"] D --> D1["实用主义——用证据优化<br/>V8(JS) / JVM(Java) / CLR(.NET)"] E --> E1["经验主义——观察现实再行动<br/>CPython / Ruby MRI"] style C fill:#69db7c,color:#fff style D fill:#ffa94d,color:#fff style E fill:#4dabf7,color:#fff
AOT:先知模型
执行前完成全部编译,生成原生机器码。 哲学立场:静态分析足以预测程序的所有行为,运行时不需要做决定。
代表:Rust、C、Go、Swift。
Go 有一个特殊设计选择:把编译速度本身作为一等目标。Go 刻意限制了泛型(早期)、放弃了模板元编程,换来秒级编译——这是 AOT 内部的取舍,不是 AOT 与其他策略的取舍。
AOT 的代价:无法利用运行时信息。一个函数在实际调用中 99% 都收到整数,但 AOT 编译器不知道,只能生成通用版本。
JIT:实用主义模型
先以基线速度运行,发现热点后动态编译优化。 哲学立场:运行时的真实行为比任何静态声明都可信,用证据优化比用推断优化更精确。
代表:V8(JavaScript)、JVM HotSpot(Java)、CLR(.NET)。
以 V8 为例的分层策略:
- Ignition(解释器):快速启动,生成字节码立即执行
- Sparkplug(基线 JIT):快速编译,生成未优化机器码
- Maglev / TurboFan(优化 JIT):对热点函数激进优化,内联、去虚化、类型特化
- 反优化(Deoptimization):优化假设失效时(如函数突然收到对象而非整数),回退到基线
JIT 的代价:预热成本(冷启动阶段性能低)、内存占用(需要同时保存字节码和机器码)、性能抖动(GC + 编译并发进行)。
解释:经验主义模型
不预先编译,逐条执行指令。 哲学立场:最大动态性,每步都保留改变语义的可能。
CPython 的 Python 是编译的(源码 → .pyc 字节码),但字节码由 PVM 逐条解释,不生成机器码。这个设计解释了为什么 Python 可以支持 eval、运行时 monkey-patching、动态导入——解释器每步执行前都能观察当前状态。
解释的代价:每一步都有解释开销(取指令、解码、分派),通常比 JIT 慢 10–100×。
特例:编译期执行
Zig 的 comptime 主动模糊了编译/运行时的界限:任何标记为 comptime 的代码在编译期执行,可以生成类型、展开循环、计算常量。这不是宏,是真正的编译期执行,完整的语言语义在编译期可用。
Zig 的哲学:与其在语言里加宏系统,不如让编译器直接执行代码。
四、谁来优化?(责任论)
连回我们的核心主线——优化的责任归属:
| 责任方 | 哲学立场 | 代表 | 代价 |
|---|---|---|---|
| 程序员 | 个体主义——你写代码,你优化 | C 汇编内联、register 提示 | 心智负担极高,错误致命 |
| 编译器 | 合同主义——类型系统证明,编译器执行 | GHC(Haskell)、rustc + LLVM | 编译慢,优化不透明 |
| 运行时 | 集体主义——运行时统一管理,按实际行为优化 | JVM JIT、V8 TurboFan | 预热成本,内存开销 |
Rust 编译慢的本质:rustc 在编译期做了其他语言留给运行时做的事——借用检查、泛型单态化(每种类型生成一份代码)、生命周期验证,再交给 LLVM 做机器相关优化。慢不是因为效率低,是因为工作量大、提前做了。
JVM JIT 的潜力:因为有运行时类型信息,JVM 可以做 AOT 无法做的优化——去虚化(devirtualization,把虚函数调用变成直接调用,前提是运行时观察到只有一种实现),再内联。高度优化的 Java 服务端代码,峰值性能可以超过手写 C++。
TypeScript 的两条管线:
- 开发期(tsc / Vite):优先速度,快速类型检查 + HMR,不做重优化
- 生产期(esbuild / rollup):优先体积和性能,tree-shaking、压缩、代码分割
五、类型与编译的交汇
编译与类型系统不是独立的两件事——类型是承诺,编译是承诺的执行者(见 01 类型系统 契约层)。
三种类型在编译期的命运:
类型擦除(Type Erasure):TypeScript 和 Java 泛型。类型信息在编译后消失,运行时无类型。TypeScript 的类型只服务于编译器,不进入运行时——这是与 JavaScript 生态兼容的必要妥协,也是零运行时开销的设计智慧。代价:运行时无法反射类型,需要 Zod 等库补偿。
编译时解析(Compile-time Resolution):Rust 的单态化。泛型 Vec<i32> 和 Vec<String> 编译出两份独立的机器码,类型信息用于优化后消失。零运行时开销,但二进制体积膨胀。
类型保留(Type Retention):Python 的 __annotations__、C# 的反射、Java 的 Class<?> 对象。类型信息保留到运行时,支持反射和动态分派。代价是运行时开销和二进制体积。
六、多语对照
| 设计问题 | 代表语言 | 核心哲学 |
|---|---|---|
| AOT + 零运行时 | C, Rust | 编译时解决一切,部署产物最简单(单一二进制) |
| 快速 AOT | Go | 编译速度本身是设计目标,秒级编译 > 极致优化 |
| JIT + 虚拟机 | Java (JVM), JavaScript (V8) | 跨平台 + 运行时自适应,峰值性能可超 AOT |
| 纯解释 | CPython, Ruby MRI | 最大动态性,eval、monkey-patch 完全支持 |
| 编译期执行 | Zig (comptime) | 彻底模糊编译/运行时界限,编译器即解释器 |
| 两阶段类型 | TypeScript | 类型只服务编译期,运行时零开销 |
| 维度 | Python (CPython) | TypeScript (V8) | Rust (LLVM) |
|---|---|---|---|
| 执行策略 | 解释执行(字节码 + PVM) | tsc 转译 → V8 JIT | AOT 编译到原生码 |
| 类型检查时机 | 运行时(可选 mypy 静态) | 编译时(tsc 强制) | 编译时(rustc 强制) |
| 类型信息命运 | 运行时保留 | 编译后擦除 | 编译后擦除(单态化) |
| 优化责任 | 运行时(CPython + 可选 JIT) | 运行时(V8 TurboFan) | 编译器(LLVM 后端) |
| 部署产物 | 源码 + 解释器 | .js bundle + Node/浏览器 | 单一二进制 |
| 启动速度 | 快 | 快(V8 快照) | 极快 |
| 峰值性能 | 中 | 高 | 极高 |
| 开发循环 | 极快(写完即跑) | 快(Vite HMR) | 中(编译等待) |
三语的编译策略差异,本质上是「何时做决定」与「谁来负责」两个哲学问题的不同答案——也是 01 类型系统 契约层在执行维度的工程落地。
延伸阅读
理论:
- Aho, Lam, Sethi, Ullman《Compilers: Principles, Techniques, and Tools》(Dragon Book)
- Lattner & Adve《LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation》(2004)
- Cytron et al.《Efficiently Computing Static Single Assignment Form and the Control Dependence Graph》(1991)
实践:
- V8 Blog — JIT 优化与分层编译详解
- rustc-dev-guide — Rust 编译器开发指南
- Zig comptime 文档 — 编译期执行的设计
关联 meta 维度
- 01 类型系统 — 类型检查时机与契约层的执行
- 03 包管理与工具链 — 构建工具链
- 06 内存管理 — 逃逸分析、GC 与所有权系统