Python 3.15 的实验性 JIT 编译器代表了 CPython 运行时架构的一次重要演进。与 PyPy、GraalPy 等独立实现的 JIT 不同,CPython 的 JIT 采用了一种被称为 "Copy-and-Patch" 的轻量级架构,这种设计在 2021 年由 Haoran Xu 和 Fredrik Kjolstad 提出,专门针对动态语言运行时的需求进行了优化。
Copy-and-Patch 架构的核心原理
传统 JIT 编译器通常采用 "中间语言 (IL) → 机器码" 的两阶段编译模式,需要复杂的编译器基础设施和大量内存开销。Copy-and-Patch 则采用了一种更直接的方法:在 CPython 编译阶段,每个字节码指令对应的 C 代码被预编译成机器码模板,这些模板中预留了运行时参数的空位(称为 "holes")。
以 LOAD_CONST 指令为例,其对应的机器码模板在编译时生成,存储在 jit_stencil.h 文件中。模板中包含两个需要填充的空位:操作数参数(OPARG)和下一条指令地址(CONTINUE)。运行时,JIT 编译器将这些模板按顺序复制到可执行内存区域,并填入实际的运行时值,形成完整的机器码序列。
这种架构的优势在于:首先,它避免了在运行时进行复杂的编译优化,显著降低了 JIT 的启动开销和内存占用;其次,机器码模板与字节码定义保持同步,修复字节码实现的 bug 同时也会修复 JIT 的对应问题,大幅降低了维护成本。
分层编译与 Tracing JIT
Python 3.15 的 JIT 采用了四层编译策略。第一层是传统的字节码解释器;第二层是 Python 3.11 引入的特化自适应解释器(Specializing Adaptive Interpreter),它根据运行时的类型信息将通用字节码替换为特化版本;第三层将特化字节码进一步分解为微操作(micro-ops),这些微操作是字节码内部更细粒度的操作单元;第四层由 JIT 将微操作编译为机器码。
在区域选择(Region Selection)策略上,CPython 采用了 Tracing JIT 而非传统的 Method JIT。Tracing JIT 通过记录程序的执行轨迹(trace)来识别热点代码,这使得编译器能够跨越函数边界进行优化,甚至内联被频繁调用的方法。例如,在一个遍历鸭子对象的循环中,Tracing JIT 可以根据实际遇到的类型(Duck 或 RubberDuck)生成特化的代码路径,并插入类型检查守卫(guard),当类型假设失效时回退到解释器执行。
然而,Tracing JIT 也带来了 "代码膨胀"(trace explosion)的风险。在分支密集的代码中,不同执行路径的组合可能导致生成大量相似的机器码片段。对于解析器、状态机等具有大量分支的工作负载,这种内存开销可能抵消性能收益。
性能边界与测试方法论
根据官方基准测试,Python 3.15 的 JIT 在 x86_64 Linux 平台上带来了约 5-6% 的几何平均性能提升,在 macOS AArch64 上则达到 11-12%。但需要注意的是,这些数字掩盖了显著的工作负载差异:某些程序可能获得显著加速,而另一些则可能略微变慢。
这种性能波动源于几个因素。首先,JIT 目前仅对包含 JUMP_BACKWARD 指令的代码路径启用,这意味着循环密集型的代码受益更多。其次,Tracing JIT 的优化效果取决于代码的类型稳定性 —— 如果运行时的类型信息频繁变化,守卫检查的开销可能超过优化带来的收益。
对于希望评估 JIT 影响的开发者,建议采用以下测试策略:使用 pyperformance 套件建立基线,重点关注 CPU 密集型工作负载;对比启用 JIT(PYTHON_JIT=1)前后的内存占用变化;对于 Web 服务等 I/O 密集型应用,JIT 的收益可能有限,应优先测试端到端延迟而非纯计算性能。
生产环境部署考量
尽管 Python 3.15 的 JIT 在架构上已相对成熟,但生产环境部署仍需谨慎。首先,JIT 代码需要可读、可写、可执行的内存页,这通过 mmap 系统调用配合 PROT_READ | PROT_WRITE | PROT_EXEC 标志实现。从安全角度,建议遵循最小权限原则:在代码生成阶段使用 PROT_READ | PROT_WRITE,生成完成后通过 mprotect 切换为 PROT_READ | PROT_EXEC。
其次,调试和剖析工具的支持仍不完善。当启用 Python 内置剖析器(如 cProfile)时,JIT 会自动禁用相关代码路径,这意味着无法直接对比 JIT 与非 JIT 代码的剖析结果。对于原生剖析器(如 Linux perf 或 GDB),JIT 生成的机器码缺乏标准的调试信息格式,栈展开(stack unwinding)可能产生不准确的调用链。
此外,Python 3.15 的 JIT 与 free-threaded Python(PEP 703)的线程安全问题仍在解决中。如果计划使用无 GIL 的 Python 构建,需要关注 GitHub issue #133171 的进展。
结论
Python 3.15 的 JIT 编译器代表了 CPython 性能优化策略的重要转向。Copy-and-Patch 架构在保持实现简洁的同时,为未来的深度优化奠定了基础。Tracing JIT 的区域选择策略虽然带来了代码膨胀的风险,但也为类型特化提供了灵活机制。
对于计划采用 JIT 的团队,建议将其视为性能优化的补充手段而非万能药。JIT 的收益在计算密集型、类型稳定的代码中最为明显;对于 I/O 密集型应用,传统的异步编程和架构优化可能带来更大收益。同时,应建立完善的基准测试体系,在真实工作负载上验证 JIT 的效果,而非仅依赖合成基准。
资料来源
- Edge, Jake. "Following up on the Python JIT." LWN.net, July 14, 2025. https://lwn.net/Articles/1029307/
- Shaw, Anthony. "Python 3.13 gets a JIT." Anthony Shaw's Blog, January 2024. https://tonybaloney.github.io/posts/python-gets-a-jit.html
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。