恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

CPython 编译器设计解析:从源码到字节码的完整流水线

  • 首页
  • 资讯中心
  • /
  • CPython 编译器设计解析:从源码到字节码的完整流水线

相关资讯

HyperFrames 合成视觉评审指南:以 style-9-prod 样张拆解配色、排版、动效与布局的取舍 2026/9/9 21:04:36
Spec Kit工作流:用需求规格化解决AI编程中的需求漂移问题 2026/9/9 21:04:36
Windows与Ubuntu双机共享键鼠实战:Barrier配置与踩坑指南 2026/9/9 21:04:36

最新资讯

PowerToys 自动更新被 GPO 或设置禁用怎么排查?
自托管 Maybe 如何更新到新版本?拉取 GHCR 镜像并重启 web worker 的步骤
C++享元模式实战:内存优化与共享对象设计
SSM旅游攻略系统开题答辩:准备、问答与避坑指南
数据仓库性能优化:从分区设计到分区裁剪的实战指南
如何用 Langflow 的 Guardrails 组件为 Agent 添加行为约束?

今日推荐

基于MongoDB的图书管理系统:数据建模与Spring Boot+Vue实战
Claude Code安装配置全攻略:从零开始用上终端AI编程助手
tmux 会话管理与终端复用:AI 编程工作流的调度中枢实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

CPython 编译器设计解析:从源码到字节码的完整流水线

发布时间:2026/9/9 21:04:36
CPython 编译器设计解析:从源码到字节码的完整流水线 CPython 编译器设计解析从源码到字节码的完整流水线【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读在 CPython 中编译指的是把 Python 源代码翻译成可被虚拟机执行的字节码bytecode这一整套过程。本文以 CPython 官方内部文档 InternalDocs/compiler.md 为核心骨架结合本仓库 Parser/、Python/、Grammar/、Include/ 等目录下的真实实现系统地讲解从词法解析 → 语法分析 → AST 生成 → 控制流图构建与优化 → 字节码发射的完整链路。读完本文你将掌握 CPython 编译器五大阶段的职责划分、各阶段核心函数与数据结构的真实调用关系并能准确指出在源码树的哪些位置可以继续深入钻研。编译的总体流程五个阶段CPython 将源代码到字节码的编译拆解为五个明确步骤这也是 InternalDocs/compiler.md 开篇给出的总览词法分析Tokenize把源代码切成 token 流实现在 Parser/lexer/ 与 Parser/tokenizer/。语法分析Parse把 token 流解析成抽象语法树AST入口是 Parser/parser.c。AST 到指令序列Compile把 AST 变换为一条条伪指令pseudo instruction实现在 Python/compile.c。控制流图与优化CFG/Optimize构造控制流图并施加窥孔优化实现在 Python/flowgraph.c。字节码发射Assemble基于优化后的控制流图产出真实字节码与各类辅助表实现在 Python/assemble.c。需要强调的是本文对解析部分只讲述理解编译所需的最小深度而把重心放在编译中后段。文档作者在原文中也提醒这是一份详细但并非穷尽的说明想要精确理解全部细节最终仍要回到源码本身。解析基于 token 流的 PEG 解析器从 Python 3.9 起CPython 的解析器是一个设计上颇为特殊的 PEG 解析器见 PEP 617链接仅作为背景说明。其特殊性在于它的输入是token 流而不是字符流——这与多数 PEG 解析器以字符为输入的习惯不同。词法分析即 tokenize把字符切成NAME、NUMBER、STRING、、:等 token与语法分析是两个独立阶段Python 语言的文法定义位于 Grammar/python.gram。例如import_name规则在第 227 行import_name[stmt_ty]: | lazylazy? import adotted_as_names { _PyAST_Import(a, lazy ? 1 : 0, EXTRA) }字面 token如:、数字、关键字的定义位于 Grammar/Tokens。文法中的(memo)标志表示该规则会被记忆化用于提升回溯性能。搜索 Grammar/python.gram 可见simple_stmt、import_stmt、block、expression、disjunction、arguments等大量规则都带上了该标志。从这两份文件出发会自动生成若干 C 文件其中就包括 Parser/parser.c。生成器是 Tools/peg_generator/pegen/c_generator.py。关于解析器更深入的设计可参见 InternalDocs/parser.mdParser 使用指南与 InternalDocs/changing_grammar.md如何修改 CPython 文法。抽象语法树与 ASDL 规范**抽象语法树AST**是对程序结构的高层表示它不必携带原始源码字符可以被理解为源码的抽象化表达。AST 节点的规格说明使用 **Zephyr 抽象语法定义语言ASDL**书写节点定义文件在 Parser/Python.asdl。每个 AST 节点语句、表达式以及列表推导、异常处理器等专门类型都由 ASDL 定义大多数定义对应某个具体的源码结构如if语句、属性访问且独立于任何具体编程语言的实现。原文给出的一段 ASDL 片段直观展示了这门语言的语法module Python { stmt FunctionDef(identifier name, arguments args, stmt* body, expr* decorators) | Return(expr? value) | Yield(expr? value) attributes (int lineno) }解读要点这段定义描述了两类语句与一个表达式函数定义、return 语句、yield 表达式。它们都被|分隔归并为stmt类型。参数修饰符规定了取值个数?表示可选0 或 1 个*表示 0 个或多个不带修饰符表示必填且只能 1 个。例如FunctionDef的name是identifierargs是argumentsbody是 0 个或多个stmtdecorators是 0 个或多个expr。注意arguments虽是一个节点类型但它作为整体出现单一 AST 节点而不是像stmt那样的一串节点。attributes (int lineno)前没有|说明这是所有三种 kind 共有的属性attribute。上述语句定义会生成如下 C 结构typedef struct _stmt *stmt_ty; struct _stmt { enum { FunctionDef_kind1, Return_kind2, Yield_kind3 } kind; union { struct { identifier name; arguments_ty args; asdl_seq *body; } FunctionDef; struct { expr_ty value; } Return; struct { expr_ty value; } Yield; } v; int lineno; }随之生成的还有一系列构造函数它们负责分配本例中的stmt_ty结构并做相应初始化kind字段指明 union 中哪个成员被初始化。例如FunctionDef()构造器会把kind置为FunctionDef_kind并初始化name、args、body与属性字段。这些生成代码落在 Python/Python-ast.c 与 Include/internal/pycore_ast.h。编译器的内存管理Arena 机制在深入编译实现之前必须先理解编译器如何处理内存。为了简化内存管理CPython 使用arena竞技场把所有内存池化到一处统一分配与回收从而省去了显式释放所有相关代码集中在 Include/internal/pycore_pyarena.h 与 Python/pyarena.c。PyArena_New()创建新 arena返回的PyArena结构会记录交给它的所有内存指针替编译器完成哪些内存需要在结束时释放的记账。该释放动作由PyArena_Free()完成只需在编译器退出的关键位置调用一次即可。一般来说只要不碰编译器的核心关键部位内存管理可以被完全忽略只有工作在编译最前端或最末端时才需要关心 arena 的运作方式。唯一的例外出现在管理PyObject时由于 CPython 其余部分基于引用计数arena 为此增加了额外支持用来清理每个已分配的PyObject——这种情形非常少见。如果你确实分配了PyObject必须通过PyArena_AddPyObject()告知 arena。这种设计与编译时大量节点在 arena 中一次性分配、随 arena 整体释放的模型高度一致避免在生成 AST、符号表等图状结构时陷入逐节点释放的泥潭。从源码到 AST解析动作的落地细节AST 由源码生成入口函数是 Parser/peg_api.c 中的_PyParser_ASTFromString()第 6 行或_PyParser_ASTFromFile()第 20 行。它们内部会调用_PyPegen_run_parser_from_string等函数驱动整个解析器。在经历若干前置检查后Parser/parser.c 中的辅助函数开始对收到的源码应用产生式规则把源码转成 token并递归地将 token 与对应规则匹配。每次匹配到某条产生式就会调用该规则对应的规则函数。这些函数统一命名为xx_rulexx即该文法规则名由 Tools/peg_generator/pegen/c_generator.py 依据 Grammar/python.gram 自动推导生成。每个规则函数在解析过程中顺带创建 AST 节点分配它所需的新节点、为必要的支撑结构调用对应的节点构造函数并连接它们。如此往复直到所有非终结符被终结符替换完毕。一旦出错规则函数回溯并尝试另一条规则若无规则可试则设置错误并终止解析。AST 节点构造辅助函数的命名是_PyAST_{xx}xx为节点名。它们由 ASDL 文法派生实现在 Python/Python-ast.c该文件由 Parser/asdl_c.py 从 Parser/Python.asdl 生成。最终结果是一串存放在asdl_seq结构中的 AST 节点。为了演示整条链路原文展示了一个处理import sys这类简单命名导入语句的规则函数当前仓库文法第 227 行的import_name已演进为支持lazy前缀并调用_PyAST_Import(a, lazy ? 1 : 0, EXTRA)此处代码为说明用途而简化、省略了错误检查与调试代码并以...表示删除的部分注释亦非原码所有// 该规则函数对应的产生式来自 python.gram // import_name: import dotted_as_names static stmt_ty import_name_rule(Parser *p) { ... stmt_ty _res NULL; { // import dotted_as_names ... Token * _keyword; asdl_alias_seq* a; // 词法tokenize步骤。 if ( (_keyword _PyPegen_expect_token(p, 513)) // tokenimport (a dotted_as_names_rule(p)) // dotted_as_names ) { ... // 为 import 语句生成 AST。 _res _PyAST_Import ( a , ...); ... goto done; } ... } _res NULL; done: ... return _res; }可见规则函数的骨架是调用_PyPegen_expect_token()之类辅助函数吞掉关键字 token递归调用子规则函数最后把子结果组装成一个_PyAST_*AST 节点。为了提升回溯性能带(memo)标志的规则会被记忆化每条此类规则函数先检查是否存在已记忆的结果命中则直接返回否则才按上述流程继续。ASDL 序列类型及其宏编译器里大量使用asdl_xx_seq *类型xx为 ASDL 序列的元素类型名。其中三种基础类型generic、identifier、int是手工维护的定义于 Python/asdl.c 及其头文件 Include/internal/pycore_asdl.h。对应的创建函数_Py_asdl_generic_seq_new(Py_ssize_t, PyArena *)为指定长度的asdl_generic_seq分配内存_Py_asdl_identifier_seq_new(Py_ssize_t, PyArena *)同上类型为 identifier_Py_asdl_int_seq_new(Py_ssize_t, PyArena *)同上类型为 int。其余 ASDL 序列类型由 Parser/asdl_c.py 自动生成见 Include/internal/pycore_ast.h。访问这些序列的宏asdl_seq_GET(asdl_xx_seq *, int)取出指定位置的元素asdl_seq_SET(asdl_xx_seq *, int, stmt_ty)把指定索引写入给定值。部分宏还有对应的无类型版本适合需要操作泛型 ASDL 序列的函数asdl_seq_GET_UNTYPED(asdl_seq *, int)从无类型asdl_seq取元素asdl_seq_SET_UNTYPED(asdl_seq *, int, stmt_ty)向无类型序列写入asdl_seq_LEN(asdl_seq *)返回asdl_seq或asdl_xx_seq的长度。官方建议优先使用带类型版本它们在调试模式下会执行检查并能帮助排查因从void *错误强转引起的 bug。此外如果工作在语句层面还需要关注语句来源的行号——目前行号作为最后一个参数传给每个stmt_ty构造函数。控制流图CFG优化的中间表示控制流图Control Flow GraphCFG是一个有向图用于刻画程序的执行流向。CFG 的节点不是单条字节码指令而是一段总是顺序执行的字节码序列这样的节点称为基本块basic block它必须从头到尾整体执行开头是唯一入口、结尾是唯一出口。若某指令a需要跳转到指令b则a必须位于其基本块的末尾而b必须位于其基本块的开头。原文用一段 Python 代码说明基本块的组织方式if x 10: f1() f2() else: g() end()对应的 CFG 中x 10这个守卫自成一块它比较x与10以基于比较结果的条件跳转结尾该跳转让这个块能同时指向if分支体和else分支体。if基本块包含f1()与f2()的调用并指向end()所在基本块else基本块包含g()调用同样指向end()块。守卫、if体或else体内更复杂的代码可能被切分为多个基本块。例如if x or y:这种短路布尔逻辑会先产生一个测试x真值的块然后分叉指向if体起始块以及另一个测试y真值的块。CFG 之所以值得作为编译中间表示是因为它是做优化时很方便的数据结构跳转关系被显式建模为图的边便于做可达性分析、死代码消除、指令重排与复制等窥孔优化。从 AST 到 CFG 再到字节码第一步构建符号表AST 到字节码的转换由 Python/compile.c 中的_PyAST_Compile()启动该函数位于第 1528 行bltinmodule.c、pythonrun.c都通过它编译源码。首个步骤是构建符号表实现在_PySymtable_Build()Python/symtable.c 第 416 行。_PySymtable_Build()首先进入传入 AST 的起始代码块然后按节点类型调用相应的symtable_visit_{xx}函数xx为 AST 节点类型名。随后通过symtable_enter_block()与symtable_exit_block()进出各类代码块在遍历 AST 树的同时划定局部变量的作用域范围。可以印证的是Python/compile.c 在new_compiler()中先调用_PyAST_Preprocess做预处理再于第 148 行调用_PySymtable_Build()得到符号表并存入编译器上下文这正对应文档描述的先后次序。第二步AST 到伪指令序列符号表就绪后compiler_codegen()Python/compile.c 第 864 行把 AST 变换成伪指令序列。伪指令与真实字节码相似但部分更抽象会在后续阶段再解析成真正字节码。指令序列的构造由一系列按 AST 节点类型分工的函数完成它们统一命名为codegen_visit_{xx}xx为stmt、expr等节点类型名。每个函数接收一个struct compiler *与对应的{xx}_ty节点典型的实现是一个巨型switch语句按传入节点的 kind 分支处理——简单逻辑内联完成复杂变换则转交名为codegen_{xx}的辅助函数xx为该处理逻辑的描述名如list、boolop。变换任意 AST 节点时使用VISIT()宏它会按传入的节点类型调用对应的codegen_visit_{xx}即VISIT({c}, expr, {node})等价于codegen_visit_expr({c}, {node})。VISIT_SEQ()宏与之类似但作用于 AST 节点序列即生成节点时由*修饰符产生的参数列表。发射指令的宏均带location参数用于把指令关联到源码位置ADDOP(struct compiler *, location, int)添加一个指定操作码ADDOP_IN_SCOPE(...)类似ADDOP但同时退出当前作用域用于在 lambda 与闭包中发射返回值操作码ADDOP_I(struct compiler *, location, int, Py_ssize_t)添加带整数参数的操作码ADDOP_O(struct compiler *, location, int, PyObject *, TYPE)根据指定 PyObject 在对应对象序列中的位置来生成带参操作码但不处理名称修饰name mangling用于对names、varnames这类序列做名字查找ADDOP_N(...)同ADDOP_O但会偷走steal对 PyObject 的引用ADDOP_NAME(...)同ADDOP_O但额外处理名称修饰用于基于名称的属性加载或导入ADDOP_LOAD_CONST(struct compiler *, location, PyObject *)依据对象在 consts 表中的位置发射LOAD_CONST操作码ADDOP_LOAD_CONST_NEW(...)同ADDOP_LOAD_CONST但偷走 PyObject 引用ADDOP_JUMP(struct compiler *, location, int, basicblock *)生成跳向某个基本块的跳转。location参数是一个携带源位置的结构体通常用LOC宏从 AST 节点提取对合成指令synthetic instruction——例如函数末尾隐式补上的return None它并不对应源码中的任何一行——则使用NO_LOCATION。比较实用的辅助函数是codegen_nameop()它会查出变量的作用域并依据表达式上下文发射恰当的 load / store / delete 操作码。第三步CFG 化与窥孔优化指令序列创建完成后_PyCfg_FromInstructionSequence()Python/flowgraph.c 第 4028 行把指令序列转成 CFG_PyCfg_OptimizeCodeUnit()第 3789 行对其施加各种窥孔优化peephole optimizations_PyCfg_OptimizedCfgToInstructionSequence()第 4131 行把优化后的 CFG 再转回指令序列。这三步全部实现在 Python/flowgraph.c。在compile.c中也可以看到这些函数在codegen完成之后的成对调用如第 4238、4243 行一带的串联逻辑印证了指令序列 → CFG → 优化 → 指令序列的既定管道。第四步字节码发射与代码对象组装最后伪指令序列被转换成真正的字节码这包括把伪指令pseudo instructions改写为实际指令把跳转目标从逻辑标签转换为相对偏移构建 异常表 与位置表把字节码与辅助表连同consts、names数组、函数引用的源文件信息filename 等等元数据一起包装进一个PyCodeObject。整个过程由_PyAssemble_MakeCodeObject()完成位于 Python/assemble.c 第 785 行。代码对象编译的最终产物及其执行_PyAST_Compile()的返回值是一个PyCodeObject定义在 Include/cpython/code.h。拿到它就意味着你拥有了可执行的 Python 字节码。字节码随后由 Python/ceval.c 中的_PyEval_EvalFrameDefault()执行——这就是解释器的主求值循环eval loop。代码对象的运行时实例化执行时的栈帧等则继续依赖 InternalDocs/frames.md 所描述的帧机制。关键文件速查解析相关Parser/Parser/Python.asdlASDL 语法文件Parser/asdl.pyASDL 定义文件的解析器把 ASDL 描述读入并解析成描述它的 ASTParser/asdl_c.py从 ASDL 描述生成 C 代码产出 Python/Python-ast.c 与 Include/internal/pycore_ast.hParser/parser.cPython 3.9 引入的新 PEG 解析器由 Tools/peg_generator/pegen/c_generator.py 依据 Grammar/python.gram 生成负责从源码构建 AST包含各产生式对应的规则函数Parser/peg_api.c解释器用来从源码创建 AST 的高层函数_PyParser_ASTFromString/_PyParser_ASTFromFileParser/pegen.c供 Parser/parser.c 中函数使用的辅助函数集合同时包含提升解析错误信息质量的逻辑Parser/pegen.h上述文件的头文件定义了Parser与Token结构。编译与运行相关Python/Python/Python-ast.c创建与 ASDL 类型对应的 C 结构并包含 AST 节点的 marshalling 代码核心 ASDL 类型的 marshalling 在 Python/asdl.c由 Parser/asdl_c.py 自动生成且每次文法变更后须单独提交——其__version__值被设置为最近一次文法变更的版本号Python/asdl.cASDL 序列类型的处理代码以及 number、identifier 等核心 ASDL 类型的 marshallingPython/ast.cAST 校验_PyAST_ValidatePython/ast_preprocess.c编译前对 AST 做预处理Python/ast_unparse.c把 AST 表达式节点还原成字符串用于字符串注解Python/ceval.c字节码执行引擎eval loopPython/symtable.c从 AST 生成符号表Python/pyarena.carena 内存管理器实现Python/compile.c基于 AST 发射伪字节码Python/flowgraph.cCFG 构建与窥孔优化Python/assemble.c从伪指令序列构造代码对象Python/instruction_sequence.c表示字节码风格伪指令序列的数据结构Python/bytecodes.c操作码的单一事实来源由其生成指令调度代码与 Include/opcode_ids.h。头文件与运行时Include/、Objects/、Lib/Include/cpython/code.hPyCodeObject的定义对应实现 Objects/codeobject.cInclude/opcode.h与 Lib/opcode.py 联动、需同步修改的文件之一Include/opcode_ids.h操作码 ID 列表由 Tools/cases_generator/opcode_id_generator.py 从 Python/bytecodes.c 生成Include/internal/pycore_ast.hC 结构体的实际定义由 Python/Python-ast.c 使用经 Parser/asdl_c.py 自动生成Include/internal/pycore_asdl.hASDL 序列对应头文件Include/internal/pycore_symtable.hstruct symtable与PySTEntryObject的定义Include/internal/pycore_parser.h对应 Parser/peg_api.cInclude/internal/pycore_pyarena.h对应 Python/pyarena.cInclude/internal/pycore_magic_number.h字节码版本化的魔数MAGIC_NUMBER所在地Objects/codeobject.cPyCodeObject相关逻辑Objects/frameobject.c含frame_setlineno()用于判断是否允许在字节码两点间跳转Lib/opcode.py暴露给 Python 层的操作码工具Objects/object_layout.md3.11 及之后版本的对象布局说明。更进一步InternalDocs/code_objects.md位置表source code locations等代码对象细节InternalDocs/frames.md帧与帧栈的运行机制InternalDocs/exception_handling.md异常表的设计InternalDocs/parser.md 与 InternalDocs/changing_grammar.md解析器使用指南与文法修改指南想从用户视角观察编译产物可用标准库dis见 Lib/dis.py反汇编任何函数或代码对象直观对照本文所述的字节码形态。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号