NEE's Blog

Python 3.15 的 JIT 编译器重回正轨

March 17, 2026

本文翻译自 Python 3.15’s JIT is now back on track,原载于 Hacker News。


好消息!CPython JIT 编译器已经提前达成了我们的(相当保守的)性能目标——在 macOS AArch64 上提前了一年多,在 x86_64 Linux 上提前了几个月。

根据截至 3 月 17 日(太平洋时间)的性能测试数据,3.15 alpha 版本的 JIT 在 macOS AArch64 上比尾调用解释器快了约 11-12%,在 x86_64 Linux 上比标准解释器快了 5-6%。这些数字是几何平均值,且为初步数据。实际性能范围从 20% 的 slowdown(减速)到超过 100% 的 speedup(加速) 不等(排除 unpack_sequence 微基准测试)。

我们还没有完善 free-threading(自由线程)支持,但目标是在 3.15 或 3.16 版本中实现。JIT 现在重回正轨了。

这一路走来有多艰难,我真的无法用语言形容

曾几何时,我甚至怀疑 JIT 项目是否真能带来有意义的性能提升。回顾一下,最初的 CPython JIT 几乎没有任何加速效果:8 个月前,我写过一篇 JIT 反思文章,讲述 3.13 和 3.14 版本的 CPython JIT 经常比解释器还慢。那段时间,Faster CPython 团队的主要赞助商也停止了资助。虽然我是志愿者,不受影响,但更重要的是,我的朋友们因此受到了波及。曾有一段时间,JIT 的前景看起来相当不确定。

从 3.13 和 3.14 到现在的变化

我不会讲什么”我们凭智慧和勇气将 JIT 从失败边缘拯救回来”的英雄故事。老实说,我把很大一部分成功归功于运气——天时、地利、人和、正确的选择。说真的,如果 JIT 的任何一个核心贡献者——Savannah Ostrowski、Mark Shannon、Diego Russo、Brandt Bucher,还有我——任何一人不在其中,我不认为这能成功。

我还要提及其他活跃的 JIT 贡献者:Hai Zhu、Zheaoli、Tomas Roun、Reiden Ong、Donghee Na(可能还有遗漏)。

今天我想聊聊 JIT 项目中较少被提及的一面:人和运气。如果你想了解我们实现这些的技术细节,可以看这里

第一部分:社区协作

Faster CPython 团队在 2025 年失去了主要赞助商。我立即提出了社区托管的想法。当时,我相当不确定这是否可行。JIT 项目向来不是新贡献者的友好领域——历史上,它需要大量的先验专业知识。

在剑桥举行的 CPython 核心冲刺会上,JIT 核心团队碰了个头,制定了计划:3.15 版本实现 5% 的加速,3.16 版本实现 10% 的加速,同时支持 free-threading。还有一个不那么引人注目但至关重要的目标:降低”巴士系数”(bus factor)。我们希望在 JIT 的三个阶段(前端/region selector、中端/optimizer、后端/code generator)都有至少 2 名活跃维护者。

在此之前,JIT 的中端只有 2 名活跃的常规贡献者。如今,JIT 的中端已有 4 名活跃贡献者,我认为其中 2 名非核心开发者(Hai Zhu 和 Reiden)是能干且受重视的成员。

吸引贡献者的方法

行之有效的是常规的软件工程实践:将复杂问题分解为可管理的小部分。

Brandt 在 3.14 版本就开了这个头,他创建了多个”巨型 issue”,将 JIT 优化拆分为简单任务。比如我们会说”尝试优化 JIT 中的单个指令”。我继承了 Brandt 的想法,在 3.15 版本继续推进。幸运的是,我的任务相对容易——涉及将解释器指令转换为易于优化的形式。为了鼓励新贡献者,我还写下了非常详细、可立即执行的说明,并清晰划分工作单元。

我相信这确实有帮助——我们有 11 名贡献者(包括我)参与这个 issue,将几乎整个解释器转换为对 JIT 优化器更友好的形式。

核心在于:JIT 可以从一个不透明的”黑盒”变成 C 程序员无需 JIT 经验就能贡献代码的项目

其他有效做法:鼓励大家、无论成就大小都一起庆祝。每个 JIT PR 都有明确的成果,我猜这给了大家方向感。

社区优化努力得到了回报。在那段时间里,JIT 在 x86_64 Linux 上从 1% 的加速提升到了 3-4% 的加速:

JIT Performance Improvement

第二部分:幸运的技术赌注

Trace Recording(追踪记录)

再说一次,我把很多成功归功于运气。在剑桥的 CPython 核心冲刺会上,Brandt 激将我,让我把 JIT 前端重写成 tracing 模式。起初我不喜欢这个主意,但带着一种”我不服”的心态,我想重写一下证明给他看这行不通。

初始原型 3 天就做出来了,但花了一个月才让它正确地进行 JIT 编译并通过测试套件。最初的结果很惨——在 x86_64 Linux 上慢了约 6%。我正准备放弃这个想法时,一个幸运的意外发生了:我误解了 Mark 的一个建议

Mark 之前建议将 dispatch table(调度表)贯穿解释器,这样解释器就有两个调度表(一个是普通解释器,一个用于 tracing)。Mark 建议让 tracing table 成为普通指令的 tracing 版本。

但是,我误解了他的意思,想出了一个更极端的版本:不是普通指令的 tracing 版本,而是只用一个指令负责 tracing,第二个表中的所有指令都指向它。是的,我知道这部分很令人困惑,希望有一天能解释得更清楚。

结果这被证明是一个非常好的选择。我发现最初的双表方法之所以那么慢,是因为解释器的大小翻倍,导致编译后的代码膨胀,自然就慢了。通过只用一个指令和两个表,我们只让解释器增加 1 条指令的大小,同时保持基础解释器超快。我亲切地称这个机制为 dual dispatch(双重调度)

Trace recording interpreter 的设计还有很多细节。稍微自夸一下,我真的认为这是一个小型艺术品。我花了一周时间迭代,直到它整体变得更快。使用 dual dispatch 后,从慢 6% 变成大致没有加速。之后,我又解决了 tracing interpreter 中一堆慢的边缘情况,最终让它快了 1.x%。

Tracing 解释器本身只比 specializing interpreter 慢 3-5 倍(根据我的估计)。关键在于它尊重 specializing interpreter 的所有正常行为,基本不会干扰它。

举个例子说明 trace recording 有多重要:它将 JIT 代码覆盖率提高了 50%。这意味着所有未来的优化可能效果会降低约 50%(假设所有代码执行相同的情况,当然这不是真的,请见谅)。

所以我要感谢 Brandt 和 Mark 引导我偶然发现了这么好的解决方案。

Reference Count Elimination(引用计数消除)

我们早期做的另一个幸运赌注是尝试引用计数消除。这也是 Matt Page 之前在 CPython 字节码优化器中做的工作。我注意到即使在字节码优化器的工作之后,JIT 代码中每次引用计数递减仍然有一个分支。我想:”为什么不尝试消除这个分支”,完全不知道会有多大帮助。

结果发现,单个分支实际上相当昂贵,随着时间累积影响很大。尤其是每个 Python 指令都可能有 >=1 个分支!

另一个幸运的部分是这个工作很容易并行化,而且是教人们了解解释器和 JIT 的好工具。这是我们在 Python 3.15 JIT 中引导人们工作的主要优化。虽然主要是一个手动重构过程,但它教会了人们需要了解的 JIT 关键部分,而不会让他们感到不知所措。

第三部分:一支优秀的团队

我们有一支很棒的基础设施团队。我半开玩笑地说,因为它只有一个人。实际上,我们的”团队”目前是 Savannah 衣柜里运行的 4 台机器。尽管如此,Savannah 完成了一个完整基础设施团队的工作量。如果我们没有地方报告性能数据,JIT 不可能进展这么快。每日 JIT 运行在反馈循环中改变了游戏规则——它帮助我们捕捉 JIT 性能回归,让我们知道优化确实有效。

Mark 技术精湛,我觉得互联网给他的赞扬已经够多了,所以我就不多说了 :)。

Diego 也很棒。他负责 ARM 硬件上的 JIT,最近还开始让 JIT 对 profiler(性能分析器)更友好。我不能强调这个问题有多难。

Brandt 为我们的机器码后端奠定了原始基础,没有它,新贡献者就得写汇编了——这可能会吓退更多人。

第四部分:与人交流

我还想鼓励与人交流和分享想法的想法。

特别感谢 CF Bolz-Tereick,他教了我很多关于 PyPy 的知识。我花了几个月研究 PyPy 的源代码,我相信这让我成为了更好的 JIT 开发者。CF 在我需要帮助时非常有帮助。

我还参与了一个友好的编译器聊天群,有 Max Bernstein,没有它我很可能早就失去动力了。Max 是一位多产的作者,也是一位友好的编译器专家。

想法不是在真空中产生的。我猜正是因为和一群编译器专家混了一段时间,我变得更擅长写 JIT 了。至少,研究 PyPy 拓宽了我的视野!

结语

人是重要的,再加上一些运气,JIT go brrr(JIT 跑得飞快)!


总结

Python 3.15 的 JIT 编译器成功背后有几个关键因素:

  1. 社区协作:将复杂问题拆解为小任务,让无 JIT 经验的 C 程序员也能贡献
  2. 技术突破:dual dispatch 机制和 trace recording 大幅提升性能
  3. 引用计数消除:看似简单的分支消除带来了显著性能提升
  4. 团队努力:基础设施、ARM 支持、profiler 友好性等工作缺一不可
  5. 开放交流:与 PyPy 等其他项目的交流带来了宝贵灵感

对于我们 Python 开发者来说,3.15 版本值得期待——JIT 终于能带来实实在在的性能提升了!

comments powered by Disqus