2024年10月2日,Python核心开发者和社区将发布 CPython v3.13.0[1] - 这是一个重大版本。(更新:发布日期已 推迟到10月7日[2] 。)
那么这个版本有什么不同,为什么你应该关注它呢?
简而言之,Python在核心运行层面做了两个重大改变,这些改变有可能从根本上改变未来Python代码的性能表现。
这些改变包括:
- 一个"自由线程"版本的CPython,允许你禁用全局解释器锁(GIL)
- 支持实验性的即时(JIT)编译
那么这些新特性是什么,它们会对你产生什么影响呢?
全局解释器锁(GIL)
GIL是什么?
从Guido Van Rossum在80年代末在阿姆斯特丹东部的一个科技园创造Python编程语言开始,它就被设计和实现为一种单线程解释型语言。这究竟意味着什么?
你经常会听说编程语言有两种类型 - 解释型和编译型。那么Python属于哪一种呢?答案是:两者都是。
你很少会发现一种纯粹由解释器从源代码直接解释的编程语言。对于解释型语言,人类可读的源代码几乎总是被编译成某种中间形式,称为字节码。然后解释器查看字节码并一条一条地执行指令。
这里的"解释器"通常被称为"虚拟机",特别是在其他语言中,如Java也做同样的事情,比如 Java字节码[3] 和 Java虚拟机[4] 。在Java和 相关语言[5] 中,更常见的做法是直接分发编译好的字节码,而Python应用程序通常是以源代码形式分发(尽管如此,现在包也经常以 wheel[6] 和 sdist[7] 的形式部署)。
这种意义上的虚拟机在各种意想不到的地方都会出现,比如PostScript格式(PDF文件本质上是编译后的PostScript)和字体渲染1。
如果你曾经注意到Python项目中有一堆*.pyc
文件,这就是你的应用程序的编译字节码。你可以像查看Java类文件一样反编译和探索pyc
文件。
💡Python vs CPython
我已经能听到一群挑剔的Python爱好者在喊"Python和CPython不一样!",他们说得对。这是一个需要明确的重要区别。
Python是编程语言,本质上是一个规定语言应该做什么的规范。
CPython是这个语言规范的参考实现,我们在这里讨论的主要是关于CPython实现。还有其他Python实现,比如一直使用JIT编译的 PyPy[8] ,运行在JVM上的 Jython[9] 和运行在.NET CLR上的 IronPython[10] 。
话虽如此,几乎每个人都只使用CPython,所以我认为在我们实际讨论CPython时谈论"Python"是合理的。如果你不同意,请在评论中发表意见或给我写一封措辞强烈的电子邮件,使用一种咄咄逼人的字体(也许是 Impact[11] ;我一直认为 Comic Sans[12] 有一种微妙的威胁感)。
所以当我们运行Python时,python
可执行文件会生成一串指令的字节码,然后解释器会一条一条地读取和执行这些指令。
如果你尝试启动多个线程,会发生什么?嗯,这些线程都共享相同的内存(除了它们的局部变量),所以它们都可以访问和更新相同的对象。每个线程将使用自己的栈和指令指针执行自己的字节码。
如果多个线程同时尝试访问/编辑同一个对象会发生什么?想象一个线程正在尝试向字典添加内容,而另一个线程正在尝试从中读取。这里有两个选择:
- 使字典(和所有其他对象)的实现线程安全,这需要大量工作,并且会使单线程应用程序变慢,或者
- 创建一个全局互斥锁(又称mutex),只允许一个线程在任何时候执行字节码。
后一种选择就是GIL。前一种选择是Python开发者所称的"自由线程"模式。
值得一提的是,GIL使垃圾收集变得更简单和更快。我们这里没有时间深入探讨垃圾收集的细节,因为这本身就是一个很大的话题,但简化版本是Python会记录对特定对象的引用数量,当计数达到零时,Python知道可以安全地删除该对象。如果有多个线程同时创建和删除对不同对象的引用,这可能导致竞争条件和内存损坏,所以任何自由线程版本都需要对所有对象使用原子计数的引用。
GIL还使开发Python的C扩展变得更容易(例如使用名字令人困惑的 Cython[13] ),因为你可以对线程安全做出一些假设,这会让你的生活更轻松,查看 py-free-threading移植C扩展指南[14] 了解更多细节。
为什么Python有GIL?
尽管近年来人气飙升,但它并不是一种特别新的语言 - 它诞生于80年代末,第一个版本于1991年2月20日发布(比我稍微大一点)。那时候,计算机看起来很不一样。大多数程序都是单线程的,单个核心的性能呈指数级增长(参见著名的 摩尔定律[15] )。在这种环境下,为了线程安全而牺牲单线程性能是没有意义的,因为大多数程序都不会利用多个核心。
此外,实现线程安全显然需要大量工作。
这并不是说你不能在Python中利用多个核心。这只是意味着你必须使用多进程(即 multiprocessing
[16] 模块)而不是使用线程。
多进程与多线程的不同之处在于每个进程都是自己的Python解释器,有自己独立的内存空间。这意味着多个进程不能访问内存中的相同对象,而是必须使用特殊的结构和通信来共享数据(参见 "在进程间共享状态"[17] 和 multiprocessing.Queue
[18] )。
值得一提的是,使用多个进程比使用多个线程有更多的开销,而且共享数据也更困难。
然而,使用多线程有时并不像人们通常认为的那么糟糕。如果Python正在进行I/O操作,比如读取文件或进行网络调用,它会释放GIL,这样其他线程就可以运行。这意味着如果你正在进行大量I/O,多线程通常会和多进程一样快。只有当你受CPU限制时,GIL才会成为一个大问题。
好吧,那么他们为什么现在要移除GIL?
移除GIL是某些人多年来一直在推动的事情,但主要原因不是它需要的工作量,而是随之而来的单线程程序性能下降。
如今,计算机单核性能的增量改进每年变化不大(尽管在定制处理器架构方面正在取得重大进展,例如Apple Silicon芯片),而计算机中的核心数量继续增加。这意味着程序利用多个核心变得越来越普遍,因此Python无法正确利用多线程的问题变得越来越严重。
快进到2021年, Sam Gross[19] 实现了一个 无GIL的概念验证实现[20] ,这促使 Python指导委员会[21] 提议对 PEP 703 – 使CPython中的全局解释器锁成为可选[22] 进行投票。投票结果是积极的,导致指导委员会 接受了提案[23] ,作为 分三个阶段逐步推出[24] 的一部分:
- 第1阶段: 自由线程模式是一个实验性的构建时选项,不是默认选项。
- 第2阶段: 自由线程模式得到官方支持,但仍不是默认选项。
- 第3阶段: 自由线程模式成为默认选项。
从阅读讨论中可以看出,有一个强烈的愿望不要将Python"分裂"成两个独立的实现 - 一个有GIL,一个没有 - 目的是最终在自由线程模式成为默认选项一段时间后,完全移除GIL,自由线程模式将成为唯一的模式。
在过去几年所有这些GIL vs. 无GIL的讨论进行的同时,还有一个名为"更快的CPython"的并行努力。这个项目由 Microsoft资助[25] ,由在Microsoft工作的 Mark Shannon[26] 和 Guido van Rossum[27] 本人领导。
这个团队的努力产生了一些非常令人印象深刻的结果,特别是 3.11[28] 版本,相比3.10版本有显著的性能提升。
随着社区/委员会的支持、多核处理器的普及以及更快的CPython努力的结合,现在是开始GIL采用计划第1阶段的最佳时机。
性能表现如何?
我在我的机器上 - MacBook Pro with Apple M3 Pro (CPU有6个性能核心和6个效率核心) - 和一个安静的EC2实例上 - t3.2xlarge (8 vCPUs) - 运行了一些基准测试。
下面的图表显示了Python 3.12和有/无GIL的Python 3.13之间在CPU密集型任务(收敛Mandelbrot集)的运行时性能比较。
解释一下这些运行时间的含义:
3.12.6
– Python 版本 3.12.6。3.13.0rc2
– Python 3.13.0 发布候选版本 2 的默认构建(写作时的最新版本)。3.13.0rc2t-g0
– 在构建时启用实验性无锁线程的 Python 3.13.0 发布候选版本 2,使用-X gil=0
参数运行,从而确保即使导入的库未标记为支持,GIL 也会被禁用。3.13.0rc2t-g1
– 在构建时启用实验性无锁线程的 Python 3.13.0 发布候选版本 2,使用-X gil=1
参数运行,从而在运行时"重新启用"GIL。
对此有几点注意事项:
- 我没有使用正式的基准测试,只是一个简单的迭代算法。你可以在 github.com/drewsilcock/gil-perf[29] 找到运行基准测试和绘制结果的代码。自己试试看!
- 我使用 hyperfine[30] 来运行基准测试,这是一个非常好的工具,但这些不是在专用硬件上运行的正式科学基准测试。我的 MacBook 正在运行很多东西,即使是 EC2 实例也会在后台运行一些东西,尽管不会那么多。
- 这些基准测试非常有趣,值得讨论,但请记住,在现实世界中,大多数进行 CPU 密集型工作的库都使用 Cython[31] 或类似工具 - 很少有人使用原始 Python 进行非常计算密集的任务。Cython 能够在执行期间暂时释放 GIL,而且已经有一段时间了。这些基准测试并不能代表这种用例。
考虑到这一点,我们已经可以得出一些观察结果:
- 当 Python 构建时启用无锁线程支持时,性能下降明显 - 大约 20%。
- 无论你是否通过
-X gil=1
参数重新启用 GIL,性能下降都是一样的。 - 如预期,禁用 GIL 时多线程显示出显著的提升。
- 如预期,启用 GIL 的多线程比单线程慢。
- 禁用 GIL 的多线程与多进程大致相同。不过,这是一个相当简单的例子,你不需要做太多实际工作。
- Apple Silicon 芯片确实令人印象深刻。我的 M3 Pro 上的单线程性能比 t3.2xlarge 上的单线程性能快约 4 倍。我知道 t3 是为了便宜和可突发而设计的,但即便如此!如果考虑到这些设备的疯狂电池续航时间,就更令人印象深刻了2。
更新 2024-09-30:它们如何扩展?
我运行了一些额外的基准测试,看看性能如何随线程/进程数量扩展。以下是以秒为单位的图表:
(不要问我的 MacBook 上第 23 块发生了什么,显然有什么东西决定在后台占用大量 CPU。)
正如预期的那样,改变线程数量不会改变启用 GIL 的运行时的性能,而禁用 GIL 的运行时和多进程模式都给出了一个漂亮的曲线,趋向于最小执行时间,其中非并行部分和硬件限制(即物理核心数)限制了性能改进。
我有点惊讶的是,对于多线程和多进程模式,性能继续改善,远远超过物理核心数。M3 有 12 个核心,并且 不做任何同时多线程(SMT)[32] 3,而 t3.2xlarge 有 8 个 vCPU,实际上只是 4 个带 SMT 的核心[33] ,所以我不太明白为什么在 16 个线程/进程时仍然看到比 15 个更好的性能。如果你对此有想法,请留言或发邮件!
当你将其绘制为加速比时,这一点变得更加清晰:
这显示了与之前相同的数据,但每个数据点都被缩放为该运行时和模式的基准情况(线程/进程数为 1)的性能改进。
关心性能的人喜欢这样绘图,这样他们就可以将其与 阿姆达尔定律[34] 进行比较,阿姆达尔定律是通过并行化程序可以加速多少的理论极限,尽管这显然不是一个正式的性能分析,它主要只是为了好玩 😎📈
如何尝试无锁线程 Python?
在撰写本文时,Python 3.13 仍处于发布候选阶段,尚未正式发布。话虽如此,今天是 9 月 28 日星期六,发布计划在 ~10 月 2 日~ 10 月 7 日,也就是 ~星期三~ 下周四,所以不远了。(更新:更新发布日期以反映 推迟的发布计划[1] 。)
如果你想提前尝试,使用 rye[35] 是不行的,它似乎只发布已部署的版本,而 uv[36] 包含 3.13.0rc2 构建但不包含 3.13.0rc2t 构建。幸运的是, pyenv[37] 同时支持 3.13.0rc2 和 3.13.0rc2t。要自己尝试:
pyenv install 3.13.0rc2t pyenv global 3.13.0rc2t python -c "import sys; print(sys.version)"
如果你正在尝试无锁线程 Python,这里有一个提示 - 如果你不指定 -X gil=0
或 -X gil=1
,GIL 将默认被禁用,但只要导入一个不支持在没有 GIL 的情况下运行的模块,就会导致 GIL 被重新启用。我在运行基准测试时发现了这一点,因为我导入了 matplotlib,这导致 GIL 被重新启用,所有的基准测试结果都变得糟糕。如果你手动指定 -X gil=0
,即使一个包没有标记自己支持无 GIL 运行,GIL 也不会偷偷地被重新启用。
JIT (即时) 编译器
这个 Python 版本中不仅仅是 GIL 有重大变化 - 还在 Python 解释器中添加了一个实验性的 JIT 编译器。
什么是 JIT?
JIT 代表即时(Just in Time),是一种编译技术,它在执行代码之前即时生成机器代码,而不是像传统的 C 编译器(如 gcc 或 clang)那样提前(AOT)编译。
我们之前已经讨论过字节码和解释器。重要的是,在 Python 3.13 之前,解释器会一次查看一条字节码指令,并在执行之前将每条指令转换为本机机器代码。随着 JIT 编译器的引入,现在可以将字节码"解释"成机器代码一次,并根据需要更新,而不是每次都重新解释。
需要指出的是, 在 3.13 中引入[38] 的这种 JIT 被称为 "复制和修补" JIT[39] 。这是一个非常新的想法,于 2021 年在一篇名为 "复制和修补编译:高级语言和字节码的快速编译算法"[40] 的文章中提出。复制和修补相对于更高级的 JIT 编译器的核心思想是,有一个简单的预生成模板列表 - JIT 编译器将模式匹配与预定义模板匹配的字节码,如果匹配,它将修补预生成的本机机器代码。
传统的 JIT 编译器会比这个高级得多,也会占用更多内存,特别是如果你将其与像 Java 或 C# 这样大量使用 JIT 编译的语言进行比较。(这也是 Java 程序占用如此多内存的部分原因。)
JIT 编译器的优点是它们可以在代码运行时适应你的代码。例如,当你的代码运行时,JIT 编译器会跟踪每段代码的"热度"。JIT 编译器可以随着代码变得越来越热而执行增量优化,甚至可以使用有关程序实际运行方式的信息来指导它正在进行的优化(就像 AOT 编译器的配置文件引导优化所做的那样)。这意味着 JIT 不会浪费时间优化只运行一次的代码,但真正热门的代码段可以进行大量运行时信息指导的优化。
现在,Python 3.13 中的 JIT 编译器相对简单,在这个阶段不会做任何疯狂的事情,但对于 Python 性能的未来来说,这是一个非常令人兴奋的发展。
JIT 会对我产生什么影响?
从短期来看,JIT 的引入不太可能对你编写或运行 Python 代码的方式产生任何影响。但这是 Python 解释器运行方式的一个令人兴奋的内部变化,可能会导致未来对 Python 性能进行更重大的改进。
特别是,它为随时间进行增量性能改进开辟了道路,这可能会逐渐提高 Python 的性能,使其与其他语言更具竞争力。话虽如此,这仍然处于早期阶段,复制和修补 JIT 技术既新颖又轻量级,所以在我们开始看到 JIT 编译器带来显著好处之前,还需要更多的重大变化。
如何尝试 JIT?
JIT 编译器在 3.13 中是"实验性的",并且默认情况下不会构建支持(至少当我使用 pyenv 下载 3.13.0rc2 时是这样)。你可以通过以下方式启用实验性 JIT 支持:
PYTHON_CONFIGURE_OPTS="--enable-experimental-jit" pyenv install 3.13.0rc2t pyenv global 3.13.0rc2t python -c "import sys; print(sys.version); print('JIT enabled:', sys._jit_enabled)"
还有其他配置选项,你可以在 PEP 744 讨论页面[41] 上阅读(比如启用 JIT 但要求在运行时使用 -X jit=1
启用等)。
这里的测试只检查 JIT 是否在构建时启用,而不是它当前是否正在运行(例如,是否在运行时被禁用)。可以在运行时检查 JIT 是否启用,但这有点棘手。这里有一个脚本可以用来弄清楚(取自 PEP 744 讨论页面[42] )4:
import sys import dis def is_jit_enabled(): def dummy(): pass bytecode = dis.Bytecode(dummy) return any(instr.opname == 'ENTER_JIT' for instr in bytecode) print(f"Python version: {sys.version}") print(f"JIT enabled at build time: {sys._jit_enabled}") print(f"JIT enabled at runtime: {is_jit_enabled()}")
PEP 744讨论中提到了PYTHON_JIT=0/1
和-X jit=0/1
两种方式 - 我发现-X
选项完全不起作用,但环境变量似乎可以达到目的。
$ python is-jit.py
JIT enabled 🚀
$ PYTHON_JIT=0 python is-jit.py
Doesn't look like the JIT is enabled 🥱
结论
Python 3.13是一个重大版本,为运行时引入了一些令人兴奋的新概念和特性。它不太可能立即改变你编写和运行Python的方式,但在接下来的几个月和几年里,随着无锁线程和JIT变得更加成熟和完善,它们很可能会对Python代码的性能产生越来越大的影响,特别是对于CPU密集型任务。
延伸阅读
- PEP 703 – 使CPython中的全局解释器锁成为可选[21]
- py-free-threading[43]
- Python 3.13获得JIT – Anthony Shaw[44]
- PEP 744 – JIT编译[45]
- 讨论 – PEP 744: JIT编译[40]
字体渲染是一个非常复杂的话题。相信我,无论你认为字体渲染有多复杂,它都比你想象的更复杂。据我所知,大部分复杂性实际上来自于在小分辨率下漂亮地绘制文本。例如,在TrueType中,整个字体和单个字形都有与之相关的指令,这些指令由FontEngine虚拟机(也就是解释器)执行。如果你对了解更多这方面的内容感兴趣,我强烈推荐Sebastian Lague的视频 - 编码冒险:渲染文本[47] 。他制作的视频真的很棒。 TrueType参考[48] 也出人意料地易读。↩
苹果甚至没有付钱让我说这些,这只是事实。↩
有些人使用"超线程"作为SMT的通用术语,但超线程是英特尔对其SMT实现的特定品牌名称,而SMT是该技术的通用术语,所以我们应该称之为SMT。↩
我还发现一些人在网上谈论如何使用
sysconfig.get_config_var("JIT_DEPS")
,但我发现这对我来说完全不起作用。↩
参考链接
- CPython v3.13.0: https://docs.python.org/3.13/whatsnew/3.13.html
- 推迟到10月7日: https://discuss.python.org/t/incremental-gc-and-pushing-back-the-3-13-0-release/65285
- Java字节码: https://en.wikipedia.org/wiki/Java_bytecode
- Java虚拟机: https://en.wikipedia.org/wiki/List_of_Java_virtual_machines
- 相关语言: https://kotlinlang.org/
- wheel: https://packaging.python.org/en/latest/discussions/package-formats/#what-is-a-wheel
- sdist: https://packaging.python.org/en/latest/discussions/package-formats/#what-is-a-source-distribution
- PyPy: https://pypy.org/
- Jython: https://www.jython.org/
- IronPython: https://ironpython.net/
- Impact: https://www.google.com/search?q=impact+font
- Comic Sans: https://www.google.com/search?q=comic+sans
- Cython: https://cython.org/
- py-free-threading移植C扩展指南: https://py-free-threading.github.io/porting
- 摩尔定律: https://en.wikipedia.org/wiki/Moore's_law
multiprocessing
: https://docs.python.org/3/library/multiprocessing.html- "在进程间共享状态": https://docs.python.org/3/library/multiprocessing.html#sharing-state-between-processes
multiprocessing.Queue
: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue- Sam Gross: https://github.com/colesbury
- 无GIL的概念验证实现: https://lwn.net/ml/python-dev/CAGr09bSrMNyVNLTvFq-h6t38kTxqTXfgxJYApmbEWnT71L74-g@mail.gmail.com/
- Python指导委员会: https://github.com/python/steering-council
- PEP 703 – 使CPython中的全局解释器锁成为可选: https://peps.python.org/pep-0703/
- 接受了提案: https://discuss.python.org/t/a-steering-council-notice-about-pep-703-making-the-global-interpreter-lock-optional-in-cpython/30474
- 分三个阶段逐步推出: https://discuss.python.org/t/pep-703-making-the-global-interpreter-lock-optional-in-cpython-acceptance/37075
- Microsoft资助: https://pyfound.blogspot.com/2022/05/the-2022-python-language-summit_2.html
- Mark Shannon: https://us.pycon.org/2023/speaker/profile/81/index.html
- Guido van Rossum: https://gvanrossum.github.io/
- 3.11: https://docs.python.org/3/whatsnew/3.11.html#faster-cpython
- github.com/drewsilcock/gil-perf: https://github.com/drewsilcock/gil-perf
- hyperfine: https://github.com/sharkdp/hyperfine
- Cython: https://cython.readthedocs.io/en/latest/src/userguide/nogil.html
- 不做任何同时多线程(SMT): https://apple.stackexchange.com/a/425670
- 4 个带 SMT 的核心: https://aws-pricing.com/t3.2xlarge.html
- 阿姆达尔定律: https://en.wikipedia.org/wiki/Amdahl's_law
- rye: https://rye.astral.sh/
- uv: https://docs.astral.sh/uv/
- pyenv: https://github.com/pyenv/pyenv
- 在 3.13 中引入: https://github.com/python/cpython/pull/113465
- "复制和修补" JIT: https://en.wikipedia.org/wiki/Copy-and-patch
- "复制和修补编译:高级语言和字节码的快速编译算法": https://dl.acm.org/doi/10.1145/3485513
- PEP 744 讨论页面: https://discuss.python.org/t/pep-744-jit-compilation/50756
- PEP 744 讨论页面: https://discuss.python.org/t/pep-744-jit-compilation/50756/53
- py-free-threading: https://py-free-threading.github.io/
- Python 3.13获得JIT – Anthony Shaw: https://tonybaloney.github.io/posts/python-gets-a-jit.html
- PEP 744 – JIT编译: https://peps.python.org/pep-0744/
- nmstoker: https://news.ycombinator.com/item?id=41679328
- 编码冒险:渲染文本: https://www.youtube.com/watch?v=SO83KQuuZvg&pp=ygUOZm9udCByZW5kZXJpbmc%3D
- TrueType参考: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#how_works