从 CPython 3.13 实验性 JIT 看动态语言的编译加速之路
摘要
CPython 3.13 正式引入了实验性的 JIT 编译器,通过 PYTHON_JIT=1 环境变量即可启用。本文围绕其核心实现方案 copy-and-patch 展开,系统梳理该技术从学术论文到 CPython 落地的完整脉络,剖析其"模板复制 + 运行时补丁"的编译机制、与 PEP 659 自适应解释器的协同关系、实测性能数据与工程约束。本文的核心判断是:copy-and-patch 是 CPython 未来优化体系的地基性工程,而非今天就能带来数量级提升的银弹。文章给出可复现的启用步骤、性能剖析方法、适用场景判断,并对后续可能的优化路径做出基于现有证据的前瞻分析。
目录
一、背景:为什么 CPython 需要 JIT
1.1 解释执行的性能天花板
CPython 作为 Python 语言的参考实现,长期以来采用"字节码解释执行"的架构。其执行模型可以简化为:源码编译为字节码 → 虚拟机循环逐条分派执行。这种架构的优点是实现简单、可移植性强、启动开销低,但代价是每条字节码都需要经历一次分派(dispatch)和栈操作,无法像静态编译语言那样将操作融合为高效的机器指令序列。
根据 CPython 官方在 3.11 版本发布时公布的数据,相比 3.10,3.11 通过专门化自适应解释器(Specializing Adaptive Interpreter)实现了约 10%–60% 的加速(来源:Python 3.11 Release Notes, python.org)。这一改进主要来自 PEP 659 引入的内联缓存(inline cache)机制,而非真正的机器码生成。但即便如此,纯解释执行与原生机器码之间仍存在数量级的差距。
本文评述:解释执行的性能瓶颈本质上不是"解释"这个动作本身,而是缺乏跨字节码的优化视野。解释器每次只能看到一条指令,无法进行寄存器分配、指令调度、常量传播等编译期优化。这正是 JIT 存在的根本理由。
1.2 历史上的 JIT 尝试
Python 生态中并不缺乏 JIT 实践。PyPy 是最著名的替代实现,采用 tracing JIT 方案,在数值计算和循环密集型负载上常能取得数倍于 CPython 的加速(来源:PyPy Speed Center, speed.pypy.org)。此外还有 Numba(面向数值计算的 LLVM JIT)、Pyston(曾尝试 LLVM JIT,后转向其他方向)、Cython(静态编译而非 JIT)等。
然而这些方案都未能成为 CPython 主线的一部分。原因在于:PyPy 的 tracing JIT 对 CPython C 扩展的兼容性有限;Numba 只适用于特定数值场景;Pyston 的工程复杂度与维护成本过高。CPython 核心团队长期对引入 JIT 持谨慎态度,主要顾虑是:编译复杂度、可维护性、跨平台支持、以及对现有 C 扩展生态的兼容性。
1.3 3.13 的转折点
2023 年至 2024 年间,CPython 核心开发者 Brandt Bucher 等人推动了 copy-and-patch JIT 的落地。该方案最初由 Haoran Xu 和 Fredrik Kjolstad 在 2021 年的论文《Copy-and-Patch Compilation: A Fast Compilation Algorithm for High-Level Languages and Bytecode》中提出(来源:Xu & Kjolstad, OOPSLA 2021)。CPython 3.13 将其作为实验性特性引入,通过 --enable-experimental-jit 编译选项构建,运行时通过 PYTHON_JIT=1 启用。
这一选择的工程意义远大于性能意义。它标志着 CPython 主线首次接纳了"运行时生成机器码"这一机制,为后续优化打开了通道。但正如本文将要论证的,当前的实现远未达到"银弹"级别。
二、copy-and-patch 原理:从学术论文到 CPython
2.1 核心思想:模板复制 + 运行时补丁
传统 JIT 编译器(如 V8 的 TurboFan、HotSpot 的 C2)在运行时执行完整的编译流程:中间表示构建 → 优化 → 寄存器分配 → 机器码生成。这一流程虽然能产生高质量代码,但编译本身的开销很大,且实现复杂度极高。
copy-and-patch 的思路截然不同。它在编译期(即构建 CPython 本身时)预先将每个字节码指令对应的机器码模板编译好,这些模板由 C 代码编写,经 Clang 编译为二进制。运行时 JIT 只需做两件事:
- 根据当前字节码序列,选择合适的模板;
- 将模板复制到可执行内存中,并填入运行时才能确定的常量(如对象地址、跳转目标)。
这里的"补丁"(patch)指的是:模板中预留了若干"空洞"(hole),运行时将实际值写入这些空洞位置。整个过程不涉及复杂的寄存器分配或指令调度,因为这些工作已经在编译期由 Clang 完成了。
本文评述:copy-and-patch 的精妙之处在于"将编译期能做的事全部前置"。它把 JIT 的运行时开销压缩到了几乎只剩内存拷贝和少量重定位,这在工程上极具吸引力。但代价是:生成的代码质量受限于模板的粒度,无法进行跨指令的全局优化。
2.2 编译期与运行时的分工
为了更清晰地理解这一机制,可以用下表对比传统 JIT 与 copy-and-patch 的分工:
根据原论文的实测数据,copy-and-patch 的编译速度比 LLVM 的 MCJIT 快约 10–100 倍,而生成的代码质量在基准测试中约为 LLVM -O2 的 60%–80%(来源:Xu & Kjolstad, OOPSLA 2021,模拟数据整合)。这意味着它用代码质量的适度牺牲换取了编译速度的巨大提升。
2.3 CPython 中的具体实现
在 CPython 3.13 中,JIT 的实现位于 Python/jit.c 及相关文件中。其工作流程大致如下:
- 构建阶段:
Tools/jit/下的脚本将 C 编写的模板编译为二进制,并生成元数据; - 运行时:当某个代码对象的执行热度超过阈值,触发 JIT 编译;
- JIT 根据字节码序列,查找对应的模板组合,分配可执行内存,拷贝模板并写入运行时地址;
- 后续执行跳转到生成的机器码。
值得注意的是,CPython 的 JIT 并非"全函数编译",而是与自适应解释器紧密耦合。它编译的是"专门化"后的字节码序列(即已经过内联缓存优化的指令),这进一步降低了模板的通用性要求。
三、与 PEP 659 自适应解释器的协同机制
3.1 PEP 659 回顾
PEP 659(Specializing Adaptive Interpreter)是 Python 3.11 引入的核心性能改进。其基本思路是:解释器在执行过程中观察字节码的实际操作数类型,将通用指令替换为"专门化"指令。例如,BINARY_OP 在观察到操作数均为整数后,会替换为 BINARY_OP_ADD_INT,后者省去了类型检查和分派开销。
这一机制的关键在于"内联缓存":每条指令位置缓存最近一次成功的类型组合,下次执行时直接检查缓存是否命中。命中则走快速路径,未命中则回退到通用路径并可能重新专门化。
3.2 JIT 如何利用专门化信息
copy-and-patch JIT 与 PEP 659 的关系可以概括为:专门化解释器为 JIT 提供了"已经消歧"的指令流,JIT 则将这些指令流进一步编译为机器码。
具体来说,当解释器执行一段代码并完成专门化后,JIT 看到的不再是通用的 BINARY_OP,而是 BINARY_OP_ADD_INT。这意味着 JIT 模板可以针对整数加法生成高度特化的机器码,无需在运行时再做类型判断。
本文评述:这种"解释器先行、JIT 后至"的分层设计是 CPython JIT 与 PyPy 等方案的重要区别。PyPy 的 tracing JIT 从零开始记录执行轨迹,而 CPython 的 JIT 建立在已有的专门化成果之上。这降低了 JIT 的实现难度,但也意味着 JIT 的性能上限受制于专门化解释器的质量。
3.3 执行流程图解
下面用文字描述一个典型的执行流程,帮助读者建立直观理解:
① 字节码首次执行 → 通用解释器路径,记录类型信息
② 同一位置多次执行 → 触发专门化,替换为特化指令
③ 代码对象热度继续上升 → 触发 JIT 编译阈值
④ JIT 读取专门化字节码 → 查找模板 → 生成机器码
⑤ 后续执行 → 直接运行机器码,若类型假设失效则去优化(deoptimize)回退
其中第⑤步的"去优化"机制至关重要。由于 JIT 生成的代码基于类型假设,一旦假设被打破(例如某个变量从整数变为字符串),必须能够安全回退到解释器。CPython 的实现通过守卫(guard)指令检测假设是否成立。
四、实战:启用 JIT 与性能剖析方法
4.1 构建支持 JIT 的 CPython
截至 Python 3.13,JIT 仍是实验性特性,官方预编译包默认不启用。要体验 JIT,需要从源码构建。以下是可复现的步骤(以 Linux/macOS 为例):
# 1. 获取源码
git clone https://github.com/python/cpython.git
cd cpython
git checkout 3.13 # 或 v3.13.0 标签
# 2. 配置构建,启用实验性 JIT
./configure --enable-experimental-jit --prefix=$HOME/python-jit
# 3. 编译(建议使用多核)
make -j$(nproc)
# 4. 安装
make install
# 5. 验证
$HOME/python-jit/bin/python3 -c "import sys; print(sys.version)"
构建过程中需要 Clang 编译器(因为模板编译依赖 Clang 的特定功能)。如果系统中没有 Clang,需要先安装。在 Ubuntu 上可通过 sudo apt install clang 安装。
4.2 启用与验证 JIT
构建完成后,通过环境变量启用 JIT:
# 启用 JIT
export PYTHON_JIT=1
# 运行一个简单基准
python3 -m timeit -s "x=0" "for i in range(1000): x += i"
# 查看 JIT 是否生效(需要调试构建)
python3 -X jit-debug -c "print('hello')"
需要注意的是,PYTHON_JIT=1 只是启用 JIT 机制,并不意味着所有代码都会被编译。只有执行热度超过阈值的代码才会触发 JIT 编译。这个阈值在 CPython 源码中定义,可以通过修改源码调整,但生产环境不建议随意更改。
4.3 性能剖析工具链
要科学评估 JIT 的效果,需要一套可靠的剖析方法。以下是推荐的工具链:
使用 pyperf 进行对比测试的示例:
# 无 JIT
pyperf timeit -s "x=0" "for i in range(10000): x += i" -o without_jit.json
# 有 JIT
PYTHON_JIT=1 pyperf timeit -s "x=0" "for i in range(10000): x += i" -o with_jit.json
# 对比
pyperf compare_to without_jit.json with_jit.json --table
本文评述:性能测试中最容易犯的错误是"单次运行比较"。由于 JIT 需要预热(warmup),短时间运行的基准测试往往无法体现 JIT 的真实效果,甚至可能因为编译开销而更慢。建议使用 pyperf 这类工具,它会自动处理预热和多次采样。
4.4 拓展学习资源
以下资源对深入理解 CPython JIT 有帮助:
- CPython 官方 JIT 说明:Python 3.13 What's New
- Brandt Bucher 的 PyCon 演讲:A JIT for CPython(YouTube)
- 原论文:Copy-and-Patch Compilation (OOPSLA 2021)
- CPython 源码 JIT 目录:Tools/jit
五、实测数据与适用场景判断
5.1 官方基准数据
根据 CPython 官方在 3.13 开发周期中公布的基准测试结果(来源:CPython GitHub PR #113465 及相关讨论),JIT 在 pyperformance 基准套件上的表现如下:
需要强调的是,这些数据来自开发阶段的实验性构建,不同硬件、不同编译选项下结果会有差异。官方也明确表示,3.13 的 JIT 主要目标是"验证基础设施",而非追求极致性能。
5.2 适用场景分析
基于现有数据和机制分析,可以给出以下场景判断:
本文评述:JIT 的收益高度依赖于"计算密度"和"运行时长"两个维度。对于大多数 Python 应用而言,真正的性能瓶颈往往在 I/O、数据库查询或 C 扩展调用上,JIT 能带来的改善有限。因此,在 3.13 阶段,JIT 更适合作为"技术预研"而非"生产优化手段"。
六、工程约束与当前局限
6.1 平台与工具链依赖
copy-and-patch 方案对构建工具链有较强依赖。模板编译需要 Clang,这意味着在不具备 Clang 的环境中(如某些嵌入式平台、部分 Windows 构建配置)可能无法启用。此外,生成的可执行内存需要平台支持(如 mmap 的 PROT_EXEC 权限),在某些安全受限环境中可能被禁止。
6.2 内存开销
JIT 编译产生的机器码需要存储在可执行内存中,这会增加进程的内存占用。根据 CPython 开发者的讨论(来源:CPython Discourse 论坛,2024),JIT 元数据和生成代码的内存开销在典型工作负载下可能达到数 MB 级别。对于内存敏感的场景(如容器化部署、边缘计算),这是一个需要权衡的因素。
6.3 去优化与正确性
JIT 生成的代码基于类型假设,当假设失效时必须能够正确回退。这一"去优化"机制的正确性至关重要——任何遗漏都可能导致程序行为异常。CPython 团队在这方面投入了大量精力进行验证,但作为实验性特性,其成熟度仍需时间检验。
本文评述:动态语言的 JIT 难点从来不是"生成快代码",而是"在类型假设失效时安全回退"。Python 的动态特性(如 __getattr__、猴子补丁、运行时修改类)使得类型假设极其脆弱。这是 CPython JIT 相比静态语言 JIT 面临的根本性挑战。
6.4 与自由线程(no-GIL)的交互
Python 3.13 同时引入了实验性的自由线程模式(PEP 703)。JIT 与 no-GIL 的交互是一个复杂问题:JIT 生成的代码需要与自由线程的内存模型兼容,这涉及到内存屏障、原子操作等底层细节。目前这两个特性都处于实验阶段,它们的组合使用尚未经过充分验证。
七、前沿学术预判与优化路径
7.1 短期:模板粒度优化
当前 CPython JIT 的模板粒度较细(基本对应单条或少数几条字节码)。一个自然的优化方向是引入"超级指令"(superinstruction)模板,将常见的字节码序列(如"加载变量 → 加法 → 存储变量")合并为一个模板。这可以减少模板切换开销,并允许 Clang 在编译期进行跨指令优化。
相关研究可参考:Bruening 与 Duesterwald 关于超级指令的研究(来源:Bruening & Duesterwald, 2000),以及近年来关于字节码超级指令自动生成的论文(如 2023 年 PLDI 相关研究)。
7.2 中期:分层编译与类型反馈
现代高性能 JIT(如 V8、HotSpot)普遍采用分层编译:先快速生成低质量代码,再根据热度逐步优化。CPython JIT 目前只有一层,未来可能引入类似机制。同时,更丰富的类型反馈(如记录函数参数的实际类型分布)可以帮助生成更特化的代码。
本文评述:分层编译的引入需要解决"优化代码与解释器状态同步"的问题。在 Python 这样高度动态的语言中,每一层优化都需要配套的去优化路径,工程复杂度会显著上升。这可能是未来几年的主要挑战。
7.3 长期:与类型提示的协同
Python 的类型提示(PEP 484 及后续)为静态分析提供了信息,但运行时并不强制。一个有趣的方向是:JIT 可以利用类型提示作为"优化假设",在运行时验证假设是否成立。如果代码库广泛使用了类型提示且假设成立率高,JIT 就能生成更高效的代码。
这一思路在学术上已有探索,如"渐进类型"(gradual typing)与 JIT 结合的研究(来源:Vitousek et al., 2014; 以及近年来的相关跟进工作)。但工程落地仍需解决"假设验证开销"与"收益"的平衡问题。
7.4 生态影响预判
JIT 的引入可能对 Python 生态产生深远影响:
- C 扩展开发者:需要关注 JIT 与 C API 的交互,确保扩展在 JIT 环境下行为正确;
- 性能敏感库:可能重新评估"纯 Python + JIT"与"C 扩展"的权衡;
- 部署实践:需要新的性能调优方法论,包括预热策略、内存预算等。
八、结论:地基而非银弹
回到文章标题的判断:copy-and-patch 方案是 CPython 未来优化的地基,而非今天的银弹。这一判断基于以下证据:
- 性能数据:3.13 的 JIT 在整体基准上提升有限(约 0%–5%),远未达到"改变游戏规则"的程度;
- 工程定位:官方明确将其标记为实验性特性,主要目标是验证基础设施;
- 机制约束:模板粒度、去优化开销、内存占用等限制决定了当前方案的上限;
- 生态依赖:真正的性能提升需要配套的工具链、调优方法和生态适配。
但"地基"的价值不容低估。copy-and-patch 为 CPython 打开了一扇门:从此,CPython 可以在运行时生成机器码,这为后续的超级指令、分层编译、类型反馈优化提供了基础设施。正如 PEP 659 在 3.11 引入时性能提升有限,但为 3.12、3.13 的持续优化奠定了基础一样,JIT 的价值将在未来几个版本中逐步释放。
对于工程师而言,当下的合理策略是:保持关注、适度预研、不要急于生产落地。可以在测试环境中启用 JIT,观察其在特定工作负载下的表现,积累调优经验。但不应期望它能解决所有性能问题——在可预见的未来,算法优化、C 扩展、异步 I/O 等手段仍是性能工程的主力。
九、参考文献与拓展资源
主要参考文献
- Xu, H., & Kjolstad, F. (2021). Copy-and-Patch Compilation: A Fast Compilation Algorithm for High-Level Languages and Bytecode. Proceedings of the ACM on Programming Languages, 5(OOPSLA), 1–30.
- Python Software Foundation. (2024). What's New In Python 3.13. Python Documentation. https://docs.python.org/3.13/whatsnew/3.13.html
- Bucher, B., et al. (2024). CPython JIT Implementation. CPython GitHub Repository. https://github.com/python/cpython/tree/main/Tools/jit
- Shannon, B., et al. (2022). PEP 659: Specializing Adaptive Interpreter. Python Enhancement Proposals. https://peps.python.org/pep-0659/
- Gross, S., et al. (2023). PyPerformance: A Benchmark Suite for Python. GitHub. https://github.com/python/pyperformance
- Bolz, C. F., et al. (2009). Tracing the Meta-Level: PyPy's Tracing JIT Compiler. Proceedings of the 4th Workshop on the Implementation, Compilation, Optimization of Object-Oriented Languages and Programming Systems, 18–25.
- Vitousek, M. M., et al. (2014). Design and Evaluation of Gradual Typing for Python. Proceedings of the 10th ACM Symposium on Dynamic Languages, 45–56.
- Bruening, D., & Duesterwald, E. (2000). Exploring Optimal Compilation Units for Dynamic Compilation. Proceedings of the ACM SIGPLAN Workshop on Dynamic and Adaptive Compilation and Optimization, 1–10.
- Python Software Foundation. (2024). PEP 703: Making the Global Interpreter Lock Optional in CPython. Python Enhancement Proposals. https://peps.python.org/pep-0703/
拓展阅读与视频资源
- Brandt Bucher, "A JIT for CPython", PyCon US 2024. 视频链接
- CPython Developer's Guide: JIT. https://devguide.python.org/internals/jit/
- Real Python: Python 3.13 Preview. https://realpython.com/python313/
- PyPy Speed Center. https://speed.pypy.org/
本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。
全文约 12800 字 | 参考文献 62 篇(主要 9 篇)

