负责翻译的程序与负责执行的程序
一句话总结
编译器是把程序不经执行、直接翻译成另一种语言的程序,解释器则是一边读取程序一边立刻执行的程序。两者共用同样的前端(把字符切成词法单元,把词法单元组成树,再检查树的含义),到最后才分道扬镳:是“现在就计算”,还是“留下以后再计算的代码”。
为什么需要它
CPU 读不懂 x = (a + 1) * 2。它能读的只有 add、imul 这样的指令和寄存器编号。人写起来顺手的文字与机器能运行的指令之间有一段距离,总得有人来填平,而填平的时机有两种。
- 执行之前做一次(编译器)。翻译要花时间,但翻译出的结果可以反复快速运行。相当一部分错误——不存在的变量、类型不匹配、括号不成对——在用户运行程序之前就能被抓出来。
- 执行时每次都做(解释器)。不必等待翻译,可以立刻运行,但同一行代码跑一百万次,就会解释一百万次。好处是实现简单,也容易窥探运行中的状态。
实际工作中,两者是混在一起的。Python 先把源码编译成字节码(__pycache__ 里的 .pyc),再解释执行这些字节码。Java 由 javac 生成字节码,JVM 起初解释执行,只把经常运行的方法编译成机器码(JIT)。所以比起“这门语言是不是编译型语言”,“哪个阶段在什么时候做”才是更准确的问题。
工作原理
编译器不是一整块,而是一串阶段。前一个阶段的输出就是后一个阶段的输入,本课程的模块也完全按这条流水线展开。
소스 글자 ──렉서──▶ 토큰 ──파서──▶ 트리(AST) ──의미 분석──▶ 검사된 트리
(2모듈) (3모듈) (4모듈)
│
┌─────────────────────────────────┼──────────────────────────┐
▼ ▼ ▼
트리를 걸으며 실행 바이트코드 + 스택 VM 중간 표현 → 최적화 → x86-64
(5모듈) (6모듈) (7·8·9모듈)
gcc 也把同样的流水线拆成几个程序来运行。平时被一行 gcc hello.c 遮住了,看不见,但可以通过选项让它在每个阶段停下来。
| 停下来的选项 | 做了什么 | 产物 |
|---|---|---|
-E |
预处理——展开 #include,替换 #define |
.i(仍是 C) |
-S |
编译——把 C 变成汇编 | .s(文本) |
-c |
汇编——把汇编变成机器码 | .o(可重定位目标文件) |
| (无) | 链接——把目标文件和库连接在一起 | 可执行文件 |
目标文件里还留着地址尚未确定的名字。hello.o 调用的 printf 在 libc 中,所以 nm hello.o 会在这个名字前打印 U(undefined)。填上这个空缺的是链接器。只在链接阶段才会出现的错误(“undefined reference to …”)之所以单独存在,原因就在这里——编译只看一个文件,链接则看全部。
解释器和编译器是否遵守同一种含义,也需要单独确认。本实验的计算器像 C 一样把除法向 0 截断(-7 / 2 得 -3)。Python 的 // 则向下取整(-4),所以用 Python 写解释器时如果用了 //,同一个程序在解释器和编译出的可执行文件里会得出不同的答案。两种实现的含义是否一致,不会自动保证,只有实际运行并对照才能知道。本课程的评分器在几乎每个步骤都用随机输入与参考实现做对照,原因也是一样的。
在现场相遇的样子
- 构建能通过,链接却失败。头文件(
#include)只提供声明,所以编译会通过,如果在链接命令里漏掉了包含定义的库,就会出现undefined reference。知道错误出在上表的哪一个阶段,就能立刻分清该去改的地方(是源码,还是构建配置)。 - “Python 是解释型语言,所以慢”。慢的原因不是没有编译,而是每条字节码指令都要在运行时检查类型。像 PyPy 或 JVM 那样,在运行中把经常运行的部分重新编译(JIT),同样的源码有时会快几十倍。
- CI 的构建缓存。编译以文件为单位完成,链接另行进行,因此可以只重新编译改动过的文件、只重做链接,也就是增量构建。
贯穿本课程要做的语言叫“mini”。它只有 64 位整数和布尔值、let、print、if、while、fn、return,以及带优先级的二十来个运算符,仅此而已。它虽小,用来把词法分析器到 x86-64 代码串成一条线却绰绰有余,而且评分器能把每个阶段都真正运行一遍。
下一项实验要做什么
对 /opt/fixtures/mini/c/hello.c,用 gcc 在每个阶段停下,生成四个产物,再用 nm 查看目标文件里的空缺。然后在一个 rpn.py 里把 RPN 计算器做两遍:一个是切分成词法单元后用栈直接计算的解释器,一个是不执行、只跟踪栈深度的检查器,再加一个写出做同样计算的 C 代码、交给 gcc 烧制的编译器。最后用随机程序对照两条路是否得出同样的答案,并测量哪一边在什么时候更快。