⚙️ 第八章 从源文件到可执行文件
📄 一句话概括

编译器分四步:词法 → 语法 → 优化 → 生成;同一份源码在不同优化级别下产物不同,但它不会改变逻辑。

📝 本节和前面几节的关系

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 秒实验

  1. 用 -O 开关跑一次 Python 程序,看看 assert 还会不会生效。
  2. 想一想:为什么调试时通常关掉优化。

常见误区

  1. 以为优化是【让代码更漂亮】。它改变的是生成的指令序列。
  2. 以为优化绝对安全。激进的优化是编译器 bug 的高发区。
自测三问(点开对答案) 1. 编译器分哪四个阶段? —— 词法分析、语法分析、优化、生成代码。 2. 为什么要分成多层? —— 每层只解决一类问题,好实现、好调试,增加新语言时往往只需改前两层。 3. 优化能不能改变程序逻辑? —— 不能,它只能删掉或改写那些不产生影响的部分。

相关笔记