type
status
date
slug
summary
tags
category
icon
password
2024 年 10 月 2 日,Python 核心开发者和社区将发布 CPython v3.13.0 —— 这次更新可谓重磅。(更新:发布时间已推迟至 10 月 7 日。)
那么,这次发布有什么特别之处?你为什么应该关心它?
简而言之,这次版本在 Python 的核心运行机制上引入了两个重大变化,它们有可能从根本上改变 Python 代码未来的性能表现。
这两个变化是:
- 一个“自由线程(free-threaded)”版本的 CPython,它允许你禁用全局解释器锁(GIL);
- 对实验性 即时编译(Just-in-Time,JIT) 的支持。
那么,这些新特性到底是什么?它们会对你带来什么影响?
全局解释器锁(GIL)
什么是 GIL?
自从 20 世纪 80 年代末,Guido van Rossum 在阿姆斯特丹东部的一个科技园区设计并实现了 Python 编程语言以来,它就是作为一种单线程的解释型语言被创建的。那么,这到底意味着什么呢?
你可能经常听说,编程语言分为两类 —— 解释型(interpreted) 和 编译型(compiled)。那么 Python 属于哪一类?答案是:两者都是。
其实,几乎不会有完全直接从源码解释执行的编程语言。对于解释型语言来说,人类可读的源码通常都会被编译成某种中间形式,称为 字节码(bytecode)。解释器随后会逐条读取并执行这些字节码指令。
这里所说的 “解释器” 通常被称为 虚拟机(Virtual Machine)。这种机制在其他语言(如 Java)中也存在,比如 Java 会将源码编译成 Java 字节码,然后由 Java 虚拟机(JVM)执行。
在 Java 生态中,更常见的做法是直接分发编译好的字节码,而 Python 应用则通常以源码形式分发(当然,现在很多 Python 包也通过
wheel
或 sdist
等形式发布)。这种“虚拟机”的概念,其实在很多你想不到的地方都有体现,比如 PostScript 格式(PDF 文件其实就是编译后的 PostScript)和字体渲染系统中【例如 TrueType 字体也有虚拟机执行字形程序】。
如果你曾经注意到项目中出现很多
.pyc
文件,那就是 Python 程序编译后的字节码文件。你甚至可以像反编译 Java 的 .class
文件那样,对 .pyc
文件进行反编译和探索。Python vs CPython
我已经仿佛听到一群严谨的 Python 爱好者在大声抗议:“Python 和 CPython 不是一回事!”——他们说得没错。这是一个很重要的区别。
Python 是一门编程语言,本质上是一个关于语言应该如何工作的规范(specification)。
而 CPython 是这个语言规范的参考实现。我们现在谈论的,主要就是关于 CPython 这个实现的内容。实际上,还有其他一些 Python 实现,比如:
- PyPy:始终使用 JIT 编译器;
- Jython:运行在 JVM 上;
- IronPython:运行在 .NET 的 CLR 上。
话虽如此,现实中绝大多数人都在使用 CPython,所以在讨论的时候,把 “Python” 当作 “CPython” 来说,其实也是合理的。
当然,如果你不同意这种说法,欢迎你在评论区留言,或者给我写一封措辞严厉、字体凶狠的邮件(比如用 Impact,我总觉得 Comic Sans 带着一种隐隐的威胁感)。
当我们运行 Python 时,
python
可执行程序会将源码编译为字节码(bytecode),这是一系列指令的流。然后解释器会逐条读取并执行这些指令。那么,如果你尝试启动多个线程,会发生什么?
线程之间共享同一块内存(除了各自的局部变量),这意味着它们都可以访问和修改同一个对象。每个线程会使用自己的栈和指令指针执行它自己的字节码。
那如果多个线程同时访问或修改同一个对象呢?
比如一个线程正在向一个字典添加内容,而另一个线程正在读取这个字典。这种情况有两个解决方案:
- 让字典(以及其他所有对象)的实现本身具备线程安全性 —— 这需要大量工作,并且会让单线程程序变得更慢;
- 创建一个全局互斥锁(mutex),只允许一个线程在任意时刻执行字节码。
第二种方式就是 GIL(全局解释器锁)。
而第一种方式,也就是让所有东西都线程安全,就是开发者们在 Python 3.13 中实验的所谓 “自由线程(free-threading)”模式。
另外值得一提的是:GIL 也让垃圾回收(GC)机制更简单、更高效。
这里我们就不深入讨论垃圾回收了(那是一个很大的话题),但简单说,Python 会为每个对象维护一个“引用计数”。当引用计数变为 0,Python 就知道可以安全地删除这个对象了。
但如果多个线程同时在创建和释放对象的引用,就有可能出现竞态条件和内存损坏等问题。所以任何 “自由线程”版本的 Python 都必须使用原子操作来更新对象的引用计数。
此外,GIL 还大大简化了 C 扩展模块(比如用 Cython 编写的)的开发过程。因为你可以默认线程是“安全”的,不需要为并发做太多额外处理,开发起来轻松很多。如果你对这方面感兴趣,可以查阅
py-free-threading
项目中的 C 扩展移植指南。为什么 Python 会有 GIL?
尽管 Python 在过去几年里变得非常流行,但它其实并不是一门新语言 —— 它诞生于 20 世纪 80 年代末,首次发布是在 1991 年 2 月 20 日(比我还年长一点)。那个年代的计算机和现在非常不同。大多数程序都是单线程的,而且单核 CPU 的性能正以指数级增长(还记得摩尔定律吧)。
在那样的环境下,为了线程安全而牺牲单线程性能,其实是没有多大意义的,因为当时的程序几乎不会利用多核。
此外,实现线程安全当然也需要花费大量精力。
这并不意味着 Python 不能利用多核 —— 它只是意味着你不能通过线程来做到,而是得使用多个进程(比如 Python 的
multiprocessing
模块)。多进程(multi-processing) 和 多线程(multi-threading) 的区别在于:
每个进程都有自己独立的 Python 解释器和内存空间。这意味着多个进程之间无法共享同一块内存中的对象,你必须通过一些特殊机制和通信方式来共享数据(参考
multiprocessing.Queue
以及 “在进程间共享状态” 相关内容)。当然,使用多个进程也有一些额外的开销,数据共享也更复杂。
不过,多线程有时候并没有大家想象的那么糟糕。
比如当 Python 在进行 I/O 操作(如读取文件或进行网络请求)时,GIL 会被主动释放,允许其他线程运行。
这也意味着:如果你的程序主要在做 I/O 密集型的任务,那么使用多线程的性能往往可以媲美多进程。
只有在你的程序是 CPU 密集型 的时候,GIL 才会成为真正的性能瓶颈。
那么,为什么他们现在要移除 GIL?
多年来,一些人一直在推动移除 GIL,但之所以迟迟没有实施,原因并不是因为工作量太大,而是因为这会导致单线程程序的性能下降。
如今,单核性能的提升已经逐年放缓(虽然像 Apple Silicon 这类定制架构确实带来了重大进展),而与此同时,计算机中的核心数却在持续增加。这也就意味着,越来越多的程序开始需要利用多核计算,而 Python 无法很好地支持多线程 的问题也就越来越严重。
时间快进到 2021 年,Sam Gross 实现了一个无 GIL 的概念验证版本(Proof of Concept),这个成果促使 Python 的指导委员会(Steering Council)发起了对 PEP 703 的投票 —— 这个提案的标题是:
“在 CPython 中让 GIL 成为可选项”(Making the Global Interpreter Lock Optional in CPython)
投票的结果是积极的,最终委员会接受了该提案,并计划分三阶段逐步推进:
- 阶段一(Phase 1):自由线程模式作为实验性编译选项存在,默认不启用;
- 阶段二(Phase 2):自由线程模式被正式支持,但仍不是默认选项;
- 阶段三(Phase 3):自由线程模式成为默认,即默认禁用 GIL。
从讨论内容来看,开发者们强烈希望不要把 Python “分裂”成两个版本(一个有 GIL,一个没有)。他们的目标是:等自由线程模式稳定运行一段时间后,最终彻底移除 GIL,只保留自由线程模式。
在 GIL 与无 GIL 之争持续的这几年里,还有另一个并行的努力项目 —— 就是著名的 “Faster CPython” 项目。
这个项目由 微软资助,由 Mark Shannon 和 Guido van Rossum(Python 之父) 主导,他们两人目前都在微软工作。
这个团队取得了一系列令人印象深刻的成果,特别是 Python 3.11,相比 3.10 提升了显著的执行性能。
结合社区和委员会的支持、多核 CPU 的普及,以及 Faster CPython 项目的推进,这些因素共同促成了 GIL 移除计划第一阶段的启动。
What does the performance look like?
这些图表展示了在两个硬件平台上对一个 CPU 密集型任务(曼德博集合迭代) 进行的性能测试结果,比较了 Python 3.12 与 Python 3.13 的不同版本(带 GIL 和无 GIL)的运行时间。横轴是 Python 的具体运行版本,纵轴是运行该任务所需的时间(单位:秒),运行时间越短代表性能越好。


关于这些运行时版本的说明:
- 3.12.6:Python 3.12.6 正式版;
- 3.13.0rc2:Python 3.13.0 候选发布版本(Release Candidate 2),默认构建(即 GIL 启用);
- 3.13.0rc2t-g0:Python 3.13.0 rc2,在构建时启用了实验性的自由线程支持(
-disable-gil
),并通过命令行参数X gil=0
在运行时禁用了 GIL,即使导入的库未声明支持无 GIL,也强制关闭;
- 3.13.0rc2t-g1:同样是构建时启用自由线程,但通过
X gil=1
在运行时重新启用了 GIL;
一些说明和注意事项:
- 这不是严格的标准基准测试,只是使用了一个简单的迭代算法。你可以在这个项目中查看测试与图表生成的代码:github.com/drewsilcock/gil-perf,欢迎你自己动手试试;
- 我使用了
hyperfine
来执行基准测试,这是一个非常好用的工具,但这些测试并不具备“科学研究级别的精度”,测试平台并不是完全专用的硬件。我在 MacBook 上有很多其他进程运行,虽然 EC2 上干扰少一些,但也不是完全空闲;
- 请记住:这些测试并不代表真实世界中的性能表现。在实际中,大多数 CPU 密集型的库都会使用 Cython 或类似工具 来实现,而不是用纯 Python 写计算密集逻辑。Cython 早就支持在执行期间暂时释放 GIL,所以这些测试并不覆盖这种常见情况;
总结观察结果:
- 启用自由线程支持后,即使 GIL 被重新启用(
X gil=1
),性能仍有显著下降 —— 大约下降 20%;
- 多线程在 GIL 被禁用的情况下表现出显著的性能提升,这是预期内的;
- 在启用 GIL 的情况下使用多线程,比单线程还要慢,这也是可以理解的;
- GIL 被禁用时的多线程性能,与多进程基本持平 —— 当然,这个测试任务本身很简单,不涉及太多实际的进程通信或共享状态;
- Apple Silicon(M3 Pro)表现非常强劲:它在单线程任务上的表现约为 EC2 t3.2xlarge 的 4 倍快!虽然 t3 是低成本、突发型实例,但差距仍然非常明显 —— 更何况 M3 还能提供超强的续航,真的令人惊叹;
更新(2024-09-30):它们的扩展性如何?
我额外运行了一些基准测试,目的是观察当线程或进程数量增加时,性能是如何扩展(scale)的。以下是每种情况下运行时间(单位:秒)的图表:


(别问我 MacBook 上第 23 个 chunk 到底发生了什么,显然有某个东西突然疯狂占用了 CPU 😂)
正如预期的那样:
- 启用 GIL 的运行时:线程数量的变化对性能几乎没有影响;
- 禁用 GIL 的运行时和 多进程模式:性能表现出了典型的“并行加速”趋势,执行时间随着线程/进程数增加而降低,直到出现瓶颈 —— 这通常是由于程序中无法并行的部分(即串行逻辑)或 硬件限制(如 CPU 核心数)所导致的。
有一点让我颇为惊讶:
在 多线程 和 多进程 模式下,性能的提升居然 远超物理核心数量后还在继续增长。
- 我的 MacBook M3 有 12 个物理核心,并不支持 SMT(超线程);
- EC2 的
t3.2xlarge
实例有 8 个 vCPU,其实是 4 个物理核心启用了 SMT(2 线程/核心);
但即便如此,在线程或进程数量达到 16 个时,性能表现竟然比 15 个还要好,这就有点谜了。如果你知道为什么,欢迎留言或者发邮件给我!
更进一步的表现:以“加速比”形式展示
我还用“加速比(speedup fraction)”的方式绘制了性能随线程/进程数量扩展的图表:

- Apple M3 Pro 的性能扩展图
- EC2 t3.2xlarge 的性能扩展图

这些图表展示的仍然是前面同样的数据,但这次的每个数据点表示该模式(runtime + mode)在某个线程/进程数量下相对于单线程/单进程时的性能提升比例。
这种图表在性能研究中比较常见,便于与著名的 阿姆达尔定律(Amdahl's Law) 进行对比 —— 该定律描述了程序理论上在并行化后可以获得的最大加速比。当然,这不是严格意义上的性能分析,更多是“好玩 + 可视化”展示而已 😎📈
如何试用自由线程版本的 Python?
截至目前(写作时间为 2024 年 9 月 28 日,星期六),Python 3.13 仍处于候选发布阶段(release candidate),尚未正式发布。
不过也快了 —— 官方计划在 10 月 7 日(星期一的下一个星期三)正式发布(更新:发布日期已从 10 月 2 日推迟至 10 月 7 日)。
如果你想提前体验自由线程的 Python,那么你可能会发现:
rye
只提供正式发布版本,因此没有 rc2t;
uv
提供了3.13.0rc2
,但不包含自由线程构建(即rc2t
);
- 幸运的是,
pyenv
支持两个版本:3.13.0rc2
和3.13.0rc2t
(即启用自由线程构建的版本)!
使用 pyenv
体验自由线程版 Python:
⚠️ 注意事项:
- 如果你使用的是自由线程构建(
3.13.0rc2t
),默认情况下 GIL 是禁用的;
- 但如果你导入了不支持无 GIL 的模块(例如
matplotlib
),GIL 会被自动重新启用,即便你没有明确要求;
- 这种“偷偷打开 GIL”的行为会让你跑出来的基准测试结果全都不准(作者在测试中就遇到过);
- 所以如果你想确保 GIL 始终处于关闭状态,请在运行时加上参数:
这样就算导入了不支持 GIL-free 的库,GIL 也不会被“偷偷”打开。
JIT(即时编译)编译器
这次 Python 版本的重大变化不只是 GIL —— Python 解释器还引入了一个实验性的 JIT 编译器。
什么是 JIT?
JIT 是 “**Just In Time(即时)” 的缩写,指的是一种编译技术:
与传统的 AOT(Ahead Of Time,预编译)编译器(如
gcc
或 clang
)不同,JIT 是在程序运行时,临时编译生成机器码,然后立即执行。在前面我们已经提到过 字节码(bytecode) 和解释器。
在 Python 3.13 之前,解释器的工作模式是这样的:
每次执行字节码指令时,都逐条将其转换为对应的机器码并立即执行。
但现在引入了 JIT 编译器后,字节码可以只被转换成机器码一次,然后在运行过程中根据需要进行更新,不再每次都重新解释。
Python 3.13 中的 JIT 类型:Copy-and-Patch
需要特别指出的是,Python 3.13 中引入的 JIT 编译器是一个被称为 “Copy-and-Patch JIT” 的变种。
这是一个 2021 年才被提出的新概念,来自论文:
《Copy-and-patch compilation: a fast compilation algorithm for high-level languages and bytecode》
它的核心思想:
- 拥有一组 预生成的机器码模板(templates);
- JIT 编译器会扫描字节码流,如果发现有某段代码匹配某个模板;
- 就会将那段模板机器码“复制 + 修补”进来,快速完成编译。
这与传统的 JIT 编译器(如 Java 或 .NET 的)非常不同:
- 传统 JIT 非常复杂且消耗内存巨大;
- 比如 Java 程序之所以那么“吃内存”,很大一部分原因就是它背后的 JIT 系统非常激进。
JIT 编译的好处是什么?
最大的优势在于:JIT 编译器可以“自适应”代码的运行行为。
比如:
- 它会在运行时跟踪哪些代码“变热了”(被多次执行);
- 随着代码“升温”,JIT 编译器可以逐步进行优化;
- 它还可以利用运行时的信息来指导优化策略,这和静态编译器中的 PGO(Profile-Guided Optimization) 类似。
这意味着:
- 不会浪费时间去优化只运行一次的代码;
- 但真正 “热点代码” 会获得更高级、更针对实际运行的优化。
回到 Python 3.13 的现实:
- 目前这个 JIT 系统还处于相对简单的阶段;
- 不会有太“疯狂”的优化策略;
- 但这代表了 Python 性能演进的一个非常激动人心的新方向。
JIT 编译器会对我有什么影响?
短期来看,JIT 的引入并不会改变你编写或运行 Python 代码的方式。
也就是说,你不需要做任何代码修改,就可以继续像以前一样使用 Python。
但从长远来看,这次的变更是 Python 解释器内部运作方式的一次令人振奋的革新,它为未来 Python 性能的持续提升打开了大门。
为什么这很重要?
- JIT 的引入为未来 “渐进式的性能优化” 打下了基础;
- 随着时间推移,Python 的执行效率有望逐步提升,接近甚至媲美某些编译型语言的表现;
- 这对于科学计算、AI 推理、多线程并发等高性能场景意义重大。
不过要明确的是:
- 当前阶段的 JIT 实现(基于 copy-and-patch 技术)还是相对简单和轻量的;
- 如果要看到真正明显的性能收益,还需要更多的优化、积累与迭代;
- 换句话说,现在是打基础,将来的收获才会大!
总结一句话:
现在的你不用改变任何代码,但未来的 Python 可能会越来越“飞”了 —— 而 JIT 就是这一飞跃的起点。
如何试用 JIT 编译器?
在 Python 3.13 中,JIT 编译器是一个实验性功能(experimental),并 不会在默认构建中启用(比如你用
pyenv
安装 3.13.0rc2
时是没有 JIT 支持的)。开启 JIT 的方式如下:
使用 pyenv
手动启用 JIT 编译支持:
这会从 GitHub 克隆 Python 源码并构建:
检查是否启用了 JIT:
输出为:
说明你已经成功构建了启用 JIT 的 Python。
更多参数和控制方式:
在 PEP 744 的讨论页面中还提到了一些其他的配置方法,比如:
X jit=1
:运行时启用 JIT(不过实测可能不生效)
PYTHON_JIT=0/1
:通过环境变量控制是否启用 JIT(实测有效)
如何在运行时判断某段函数是否被 JIT 编译?
可以使用下面这个脚本(来自 PEP 744 讨论页):
示例运行:
小结 🧠
控制方式 | 是否有效 | 说明 |
--enable-experimental-jit | ✅ | 编译时开启 JIT 支持 |
PYTHON_JIT=0/1 | ✅ | 运行时启用/禁用 JIT |
-X jit=0/1 | ⚠️ | 目前似乎无效,尚未正式支持 |