Everything you need to know about Python 3.13 – JIT and GIL went up the hill
2025-4-4
| 2025-4-11
Words 6377Read Time 16 min
type
status
date
slug
summary
tags
category
icon
password
2024 年 10 月 2 日,Python 核心开发者和社区将发布 CPython v3.13.0 —— 这次更新可谓重磅。(更新:发布时间已推迟至 10 月 7 日。)
那么,这次发布有什么特别之处?你为什么应该关心它?
简而言之,这次版本在 Python 的核心运行机制上引入了两个重大变化,它们有可能从根本上改变 Python 代码未来的性能表现。
这两个变化是:
  1. 一个“自由线程(free-threaded)”版本的 CPython,它允许你禁用全局解释器锁(GIL);
  1. 对实验性 即时编译(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 包也通过 wheelsdist 等形式发布)。
这种“虚拟机”的概念,其实在很多你想不到的地方都有体现,比如 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),这是一系列指令的流。然后解释器会逐条读取并执行这些指令。
那么,如果你尝试启动多个线程,会发生什么?
线程之间共享同一块内存(除了各自的局部变量),这意味着它们都可以访问和修改同一个对象。每个线程会使用自己的栈和指令指针执行它自己的字节码。
那如果多个线程同时访问或修改同一个对象呢?
比如一个线程正在向一个字典添加内容,而另一个线程正在读取这个字典。这种情况有两个解决方案:
  1. 让字典(以及其他所有对象)的实现本身具备线程安全性 —— 这需要大量工作,并且会让单线程程序变得更慢;
  1. 创建一个全局互斥锁(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 ShannonGuido 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 的具体运行版本,纵轴是运行该任务所需的时间(单位:秒),运行时间越短代表性能越好。
notion image
notion image

关于这些运行时版本的说明:

  • 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,所以这些测试并不覆盖这种常见情况;

总结观察结果:

  1. 启用自由线程支持后,即使 GIL 被重新启用(X gil=1),性能仍有显著下降 —— 大约下降 20%
  1. 多线程在 GIL 被禁用的情况下表现出显著的性能提升,这是预期内的;
  1. 在启用 GIL 的情况下使用多线程,比单线程还要慢,这也是可以理解的;
  1. GIL 被禁用时的多线程性能,与多进程基本持平 —— 当然,这个测试任务本身很简单,不涉及太多实际的进程通信或共享状态;
  1. Apple Silicon(M3 Pro)表现非常强劲:它在单线程任务上的表现约为 EC2 t3.2xlarge 的 4 倍快!虽然 t3 是低成本、突发型实例,但差距仍然非常明显 —— 更何况 M3 还能提供超强的续航,真的令人惊叹;

更新(2024-09-30):它们的扩展性如何?

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

更进一步的表现:以“加速比”形式展示

我还用“加速比(speedup fraction)”的方式绘制了性能随线程/进程数量扩展的图表:
notion image
  • Apple M3 Pro 的性能扩展图
  • EC2 t3.2xlarge 的性能扩展图
notion image
这些图表展示的仍然是前面同样的数据,但这次的每个数据点表示该模式(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.0rc23.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,预编译)编译器(如 gccclang)不同,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
⚠️
目前似乎无效,尚未正式支持
  • Python
  • Database System (一)Let's code a TCP/IP stack, 5: TCP Retransmission
    Loading...