CPython 实现原理
  • README
  • 一、简介
    • 1.1 如何使用此书
    • 1.2 额外材料和学习资料
  • 二、获取 CPython 源码
    • 2.1 源代码里有什么?
  • 三、准备你的开发环境
    • 3.1 选IDE还是编辑器?
    • 3.2 安装Visual Studio
    • 3.3 安装Visual Studio Code
    • 3.4 安装JetBrains Clion
    • 3.5 安装Vim
    • 3.6 总结
  • 四、编译 CPython
    • 4.1 在 macOS 上编译 CPython
    • 4.2 在 Linux 上编译 CPython
    • 4.3 安装自定义版本
    • 4.4 make 快速入门
    • 4.5 CPython 的 make 目标
    • 4.6 在 Windows 上编译 CPython
    • 4.7 PGO 优化
    • 4.8 总结
  • 五、Python 语言和语法
    • 5.1 为什么 CPython 是用 C 语言而不是用 Python 语言来实现
    • 5.2 Python 语言规范
    • 5.3 分析器生成器
    • 5.4 重新生成语法
    • 5.5 总结
  • 六、配置和输入
    • 6.1 配置状态
    • 6.2 构建配置
    • 6.3 从输入构建模块
    • 6.4 总结
  • 七、基于语法树的词法分析和解析
    • 7.1 具象语法树生成器
    • 7.2 CPython 解析器-分词器
    • 7.3 抽象语法树
    • 7.4 要记住的术语
    • 7.5 一个示例:添加一个约等于比较运算法
    • 7.6 总结
  • 八、编译器
    • 8.1 相关源文件
    • 8.2 重要的专业术语
    • 8.3 实例化一个编译器
    • 8.4 未来标志和编译器标志
    • 8.5 符号表
    • 8.6 核心编译过程
    • 8.7 汇编
    • 8.8 创建一个 Code Object
    • 8.9 使用 Instaviz 展示 Code Object
    • 8.10 一个示例:实现约等于操作符
    • 8.11 总结
  • 九、求值循环
    • 9.1 构建线程状态
    • 9.2 构建帧对象
    • 9.3 帧的执行
    • 9.4 值栈
    • 9.5 例子:在列表中添加元素
    • 9.6 总结
  • 十、内存管理
    • 10.1 C 中的内存分配
    • 10.2 Python 内存管理系统设计
    • 10.3 CPython 内存分配器
  • 十一、并行和并发
    • 11.1 并行和并发模型
    • 11.2 进程的结构
    • 11.3 多进程并行
    • 11.4 多线程
    • 11.5 异步编程
    • 11.6 生成器
    • 11.7 协程
    • 11.8 异步生成器
    • 11.9 子解释器
    • 11.10 总结
  • 十二、对象和类型
    • 12.1 本章的例子
    • 12.2 内置类型
    • 12.3 对象和可变长度对象类型
    • 12.4 类型类
    • 12.5 布尔和整数类型
    • 12.6 Unicode 字符串类型
    • 12.7 字典类型
    • 12.8 总结
  • 十三、标准库
    • 13.1 Python 模块
    • 13.2 Python 和 C 模块
  • 十四、测试套件
    • 14.1 在 Windows 上运行测试套件
    • 14.2 在 Linux 或 MacOS 上运行测试套件
    • 14.3 测试标志
    • 14.4 运行特定测试
    • 14.5 测试模块
    • 14.6 测试工具
    • 14.7 总结
  • 十五、调试
  • 十六、基准测试、性能分析和追踪
  • 十七、下一步计划
    • 17.1 为 CPython 编写 C 扩展
    • 17.2 改进你的 Python 应用程序
    • 17.3 为 CPython 项目做贡献
    • 17.4 继续学习
  • 十八、附录
    • 18.1 C 预处理器
    • 18.2 基础 C 语法
    • 18.3 总结
  • 致谢
Powered by GitBook
On this page
  • 从 Python 访问编译器
  • 编译器 C API
  • 指令
  • 指令类型
  • 跳转指令
  • 基础帧块
  • 操作和参数
Edit on GitHub
  1. 八、编译器

8.6 核心编译过程

现在的 PyAST_CompileObject() 具有编译器状态、symtable 和 AST 形式的模块,实际的编译过程可以开始了。

核心编译器有两个目的:

  1. 将状态、symtable 和 AST 转换成控制流图(CFG);

  2. 通过捕获逻辑或代码错误来保护执行阶段免受运行时异常的影响。

从 Python 访问编译器

你可以通过调用内置函数 compile() 来调用 Python 中的编译器。它会返回一个 code object:

>>> co = compile("b+1", "test.py", mode="eval")
>>> co
<code object <module> at 0x10f222780, file "test.py", line 1>

与 symtable() API一样,简单表达式应具有 “eval” 的模式,模块、函数或类应具有 “exec” 的模式。

编译后的代码可以在 code object 的 co_code 属性中找到:

>>> co.co_code
b'e\x00d\x00\x17\x00S\x00'

标准库还包括一个 dis 模块,它可以反汇编字节码指令。你可以在屏幕上打印它们或获取指令(instruction)实例列表。

注

dis 模块中的 instruction 类型映射了 C API 中的 instr 类型。

如果你导入 dis 模块并将 code object 的 co_code 属性传入 dis() 函数,则函数将其反汇编并在交互式解释器上打印指令。

>>> import dis
>>> dis.dis(co.co_code)
0 LOAD_NAME 0 (0)
2 LOAD_CONST 0 (0)
4 BINARY_ADD
6 RETURN_VALUE

LOAD_NAME、LOAD_CONT、BINARY_ADD 和 RETURN_VALUE 都是字节码指令。它们被称为字节码,因为在二进制形式中,它们都是一个字节长。然而,自从 Python 3.6 以来,存储格式已经更改为 word,所以现在从技术上讲,它们是“字码”,而不是字节码。

字节码指令的完整列表可用于 Python 的每个版本,并且它在版本之间会更改。例如,在 Python 3.7 中引入了一些新的字节码指令,以加快特定方法调用的执行速度。

在之前的章节中,你探索了 instaviz 包。这包括了通过运行编译器来可视化 code object 类型。instaviz 还显示了 code object 内部的字节码操作。

再次执行 instaviz 以查看在交互式解释器上定义的函数的 code object 和字节码。

>>> import instaviz
>>> def example():
a = 1
b = a + 1
return b
>>> instaviz.show(example)

编译器 C API

AST 模块编译的入口点是 compiler_mod() 函数,此函数根据模块类型切换到不同的编译器函数。如果你假设 mod 是 Module,则模块将作为编译器单元编译到 c_stack 属性中。然后运行 assemble() 从编译器单元堆栈中创建 PyCodeObject。

新返回的 code object 由解释器发送出去执行,或者以 .pyc 文件的形式存储在磁盘上:

代码不翻译

compiler_body() 循环遍历模块中的每个语句并访问它:

代码不翻译

语句类型是通过调用 asdl_seq_GET() 确定的, 它会查看 AST 节点类型。

VISIT 通过宏为每个语句类型调用 Python/compile.c 中的函数:

#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
    return 0; \
}

对于 stmt (泛型类型语句),编译器将调用 compiler_visit_stmt() 并切换至能在 Parser /Python.asdl 中找到的所有潜在语句类型。

代码不翻译

例如,这是 Python 中的 for 语句:

for i in iterable:
    # block
else: # optional if iterable is False
    # block

你可以在铁路图中可视化 for 语句:

图片不翻译

如果语句是 for 类型,那么 compiler_visit_stmt() 会调用 compile_for()。所有语句和表达式类型都有一个等效的 compiler_*() 函数。更直接的类型会创建内联字节码指令,而一些更复杂的语句类型会调用其他函数。

指令

许多语句可以有子语句。for 循环有一个执行体,但你也可以在赋值和迭代器中具有复杂的表达式。

编译器将 block 传递给编译器状态。这些块包含指令序列。指令数据结构有操作码、参数、目标块(如果这是跳转指令,你将在下面了解到)和语句的行号。

指令类型

指令类型 instr 具有如下字段:

字段
类型
用途

i_jabs

unsigned

指定此跳转为绝对跳转的标志

i_jrel

unsigned

指定此跳转为绝对跳转指令的标志

i_lineno

int

创建此指令的行号

i_opcode

unsigned char

此指令表示的操作码编号(请参见 Include/Opcode.h)

i_oparg

int

操作码参数

i_target

basicblock*

i_jrel 为 true 时指向目标 basicblock 的指针

跳转指令

跳转指令用于从一个指令跳转到另一个指令,它们可以是绝对的,也可以是相对的。

绝对跳转指令 指定编译 code object 中的确切指令编号,而相对跳转指令指定相对于另一条指令的跳转目标。

基础帧块

基础帧块(类型为 basicblock)包含以下字段:

字段
类型
用途

b_ialloc

int

指令数组长度(b_instr)

b_instr

instr *

指向指令数组的指针

b_iused

int

使用的指令数(b_instr)

b_list

basicblock *

此编译单元中的 block 列表(倒序)

b_next

basicblock*

指向正常控制流到达的下一个 block 的指针

b_offset

int

块的指令偏移量,由 assemble_jump_offsets() 计算得到

b_return

unsigned

如果插入了 RETURN_VALUE 操作码,则为 true

b_seen

unsigned

用于执行基础块的深度优先搜索(请参阅“汇编”章节)

b_startdepth

int

进入块时的堆栈深度,由stackdepth() 计算得到

操作和参数

不同类型的操作需要不同的参数。例如,ADDOP_JREL 和 ADDOP_JABS 分别指“add operation with jump to a relative position” 和 “add operation with jump to an absolute position"。

还有其他宏:ADDOP_I 调用 compiler_addop_i(),它添加了一个带有整数参数的操作。ADDOP_O 调用 compiler_addop_o(),它添加了一个带有 PyObject 参数的操作。

Previous8.5 符号表Next8.7 汇编

Last updated 2 years ago