编译器分四步:词法 → 语法 → 优化 → 生成;同一份源码在不同优化级别下产物不同,但它不会改变逻辑。
8.2 看清了目标语言长什么样,这一节讲怎么翻过去。
一、四个阶段
词法分析:把源码切成一个个词——变量名、数字、符号。 语法分析:按语法把这些词搭成一棵树,看句子通不通。 优化:在不动逻辑的前提下,把代码改小、改快。 生成代码:按目标 CPU 的规则,编出一条条指令。
二、为什么要分成这么多步
因为每一层只解决一类问题,这样才好写、好调、好扩展。 词法层不认识语法,只负责切词;语法层不关心怎么优化,只负责判断句子结构。 想支持一门新语言?往往只需要改前面两层,后面两层原封不动。 这种【一层只做一件事】的做法,在整本书里已经出现过好几次了(7.7 的开机接力也是它)。
三、优化会改变产物,但不会改变结果
拿一段带 assert 的源码用两种优化级别各编一遍,指令条数就不一样。
实测:optimize=0 是 15 条指令,optimize=2 只剩 9 条——assert 那一段被整个去掉了。
去掉的理由很正当:在【关掉调试】的场合,这个检查本来就不需要做。
但要注意:优化绝不能改变程序的逻辑——它只能删掉【本来就不产生影响】的部分。
四、编译器也会出错
优化越激进,越容易出问题——这是编译器 bug 最常见的来源。 所以现实里的编译器都留着开关:出问题就降级优化,甚至完全关掉。 调试时用的【不优化】版本,和发布时用的【高度优化】版本,跑出来的现象可能不一样。 记住这条经验:碰到【只在发布版出现】的怪问题,先怀疑优化。
五、动手 Demo:编译器负责翻译源代码
编译器做的事可以拆成四步:词法 → 语法 → 优化 → 生成。 其中【优化】实实在在会改变产物。
import dis
src = "assert 1 == 1" + chr(10)
src += "x = 2" + chr(10)
src += "print(x)" + chr(10)
for opt in (0, 2):
code = compile(src, "<demo>", "exec", optimize=opt)
ops = list(dis.get_instructions(code))
print("optimize=%d -> %d 条指令" % (opt, len(ops)))
实跑输出:
--------------------------------------------------------------------
编译器负责翻译源代码
--------------------------------------------------------------------
编译器做的事可以拆成四步:
阶段 做什么
------------------------------------------------------
词法分析 把源码切成一个个词(变量名、数字、符号)
语法分析 按语法把词搭成一棵树,看句子通不通
优化 在不动逻辑的前提下把代码改小、改快
生成代码 按目标 CPU 的规则编出指令
**第三步【优化】最容易被忽略,但它实实在在会改变产物。**
拿一段带 assert 的源码,用两种优化级别编两遍:
源码:
assert 1 == 1
x = 2
print(x)
优化级别 指令条数 assert 还在吗
---------------------------------------------------------
optimize=0 15 条 在(要多比较、可能要多报错)
optimize=2 9 条 被优化掉了
**同一份源码,两种优化级别编出来的产物不一样。**
但要注意:**优化不能改变程序的逻辑**——它只是把【本来就不需要做的事】去掉。
实测:optimize=0 得 15 条指令,optimize=2 只剩 9 条——assert 那一段被优化掉了。 优化只删掉【本来就不产生影响】的部分,不会改变程序的逻辑。
30 秒实验
- 用 -O 开关跑一次 Python 程序,看看 assert 还会不会生效。
- 想一想:为什么调试时通常关掉优化。
常见误区
- 以为优化是【让代码更漂亮】。它改变的是生成的指令序列。
- 以为优化绝对安全。激进的优化是编译器 bug 的高发区。
自测三问(点开对答案)
1. 编译器分哪四个阶段? —— 词法分析、语法分析、优化、生成代码。 2. 为什么要分成多层? —— 每层只解决一类问题,好实现、好调试,增加新语言时往往只需改前两层。 3. 优化能不能改变程序逻辑? —— 不能,它只能删掉或改写那些不产生影响的部分。- ☐ 下一节(8.4 仅靠编译无法得到可执行文件)
相关笔记
- 看一看本机代码的内容 — 第四步生成的就是它
- 仅靠编译无法得到可执行文件 — 编译完还差什么
- 使用源代码进行安装 — 编译这一步的日常样子
- ⌨️ 编程原理 · 索引 — 把编程全貌理一遍
- ⚙️ 程序是怎样跑起来的 · 索引