上 2ND 2024年10月,Python核心开发者和社区将发布 CPython v3.13.0 – 这真是太棒了。 (更新:现已发布 推回到7th 十月.)
那么是什么让这个版本与众不同,为什么你应该关心它呢?
简而言之,Python 在核心级别的运行方式发生了两项重大变化,这有可能从根本上改变未来 Python 代码的性能状况。
这些变化是:
- CPython 的“自由线程”版本,允许您禁用全局解释器锁 (GIL),以及
- 支持实验性即时 (JIT) 编译。
那么这些新功能是什么以及它们会对您产生什么影响?
全局解释器锁 (GIL)
什么是 GIL?
自 80 年代末 Guido Van Rossum 在东阿姆斯特丹的一个科技园创立 Python 编程语言以来,它就被设计和实现为一种单线程解释语言。这究竟意味着什么?
您通常会听说有两种类型的编程语言:解释型和编译型。那么Python是哪个呢?答案是: 是的。
您很少会找到一种纯粹由解释器从源代码解释的编程语言。对于解释型语言,人类可读的源代码几乎总是被编译成某种中间形式,称为字节码。然后解释器查看字节码并一一执行指令。
这里的“解释器”通常被称为“虚拟机”,特别是在其他语言中,例如 Java,它与 Python 做同样的事情。 Java字节码 和 Java虚拟机。在爪哇和 朋友们,更常见的是发送编译后的字节码本身,而 Python 应用程序通常作为源代码分发(尽管如此,包通常部署为 轮子 也 sdist 如今)。
虚拟机这个词的含义出现在各种意想不到的地方,例如在 PostScript 格式(PDF 文件本质上是编译的 PostScript)和字体渲染中1。
如果你曾经注意到一堆 *.pyc
Python 项目中的文件,这是为您的应用程序编译的字节码。你可以反编译探索一下 pyc
文件的方式与查找 Java 类文件的方式完全相同。
Python 与 CPython
我已经能听到一群迂腐的 Python 狂热者齐声喊道“Python 与 CPython 不一样!”,他们是对的。这是一个重要的区别。
Python 是一种编程语言,本质上是一种说明该语言应该做什么的规范。
CPython 是 参考实现 这个语言规范的一部分,我们这里讨论的主要是 CPython 的实现。还有其他 Python 实现,例如 吡啶 一直使用JIT编译, Jython 它运行在 JVM 上并且 铁蟒 它在 .NET CLR 上运行。
话虽如此,几乎每个人都只使用 CPython,所以我认为当我们真正谈论 CPython 时,谈论“Python”是合理的。如果您不同意,请继续发表评论或给我写一封措辞强硬的电子邮件,使用激进的字体(也许 影响;我一直以为 漫画字体 有一种微妙的威胁气氛)。
所以当我们运行Python时, python
可执行文件会生成字节码,字节码是指令流,然后解释器将逐条读取并执行指令。
如果您尝试启动多个线程,会发生什么?嗯,线程都共享相同的内存(除了它们的局部变量),因此它们都可以访问和更新相同的对象。每个线程将使用自己的堆栈和指令指针执行自己的字节码。
如果多个线程尝试同时访问/编辑同一个对象,会发生什么情况?想象一下,一个线程正在尝试添加到字典中,而另一个线程正在尝试从中读取内容。这里有两个选项:
- 使 dict (以及所有其他对象)的实现成为线程安全的,这需要付出很大的努力,并且会使单线程应用程序变慢,或者
- 创建一个全局互斥锁(又名互斥锁),它在任何时候只允许一个线程执行字节码。
后一个选项是 GIL。前一个选项被 Python 开发人员称为“自由线程”模式。
还值得一提的是,GIL 使垃圾收集变得更加简单和快速。我们没有时间在这里深入探讨垃圾收集,因为它本身就是一个很大的话题,但一个简化的版本是,Python 会记录对特定对象的引用数量,以及该计数何时达到零,Python 知道它可以安全地删除该对象。如果有多个线程同时创建和删除对不同对象的引用,这可能会导致竞争条件和内存损坏,因此任何自由线程版本都需要对所有对象使用原子计数的引用。
GIL 还使得为 Python 开发 C 扩展变得更加容易(例如,使用名称容易混淆的 赛通)因为您可以对线程安全做出假设,从而使您的生活变得更加轻松,请查看 用于移植 C 扩展的 py-free-threading 指南 有关此的更多详细信息。
为什么 Python 有 GIL?
尽管在过去几年中流行度激增,但它并不是一种特别新的语言 – 它是在 80 年代末构思的,于 20 世纪 20 年代首次发布th 1991 年 2 月(比我大一点)。那时,计算机看起来非常不同。大多数程序都是单线程的,并且各个内核的性能呈指数级增长(请参阅旧的 摩尔定律)。在这种环境中,当大多数程序不使用多核时,为了线程安全而牺牲单线程性能没有多大意义。
此外,实现线程安全显然需要大量工作。
这并不是说您不能在 Python 中使用多核。这只是意味着您必须使用多个进程,而不是使用线程(即 multiprocessing
模块)。
多处理与多线程不同,因为每个进程都是自己的 Python 解释器,拥有自己独立的内存空间。这意味着多个进程无法访问内存中的相同对象,而是必须使用特殊的构造和通信来共享数据(请参阅 “进程之间共享状态” 和 multiprocessing.Queue
)。
值得一提的是,与使用多个线程相比,使用多个进程会产生更多的开销,而且共享数据更加困难。
然而,使用多线程有时并不像人们通常想象的那么糟糕。如果 Python 正在执行 I/O(例如读取文件或进行网络调用),它将释放 GIL,以便其他线程可以运行。这意味着,如果您执行大量 I/O,多线程通常会与多处理一样快。当您受到 CPU 限制时,GIL 就会成为一个大问题。
好的,那么为什么他们现在要删除 GIL 呢?
取消 GIL 是某些人多年来一直在推动的事情,但它没有被完成的主要原因不是它需要的工作量,而是它带来的相应的性能下降。 – 线程程序。
如今,计算机单核性能的增量改进每年都不会发生太大变化(尽管定制处理器架构(例如 Apple Silicon 芯片)正在取得巨大进步),而计算机中的核心数量仍在不断增加。这意味着程序使用多核变得更加常见,因此 Python 无法正确利用多线程正变得越来越成为一个问题。
快进到 2021 年, 萨姆·格罗斯 实施了一个 no-GIL 概念验证实施 这刺激了 Python 指导委员会 提议投票 PEP 703 – 在 CPython 中使全局解释器锁成为可选。投票结果是积极的,指导委员会 接受提案 作为一个 逐步推出 分三个阶段:
- 第一阶段:自由线程模式是一个实验性的构建时选项,不是默认选项。
- 第二阶段:官方支持自由线程模式,但仍不是默认模式。
- 第三阶段:默认为自由线程模式。
通过阅读讨论,我们强烈希望不要将 Python“拆分”为两个单独的实现 – 一个带有 GIL,一个没有 – 其目的是,最终在自由线程模式成为默认模式一段时间后,GIL 将被完全删除,自由线程模式将是唯一的模式。
尽管所有这些 GIL 与 no-GIL 的问题在过去几年中一直在进行,但还有一个名为“Faster CPython”项目的并行工作。这已经是 由微软资助 并由 马克·香农 和 吉多·范罗苏姆 他本人都在微软工作。
该团队一直以来的努力取得了一些非常令人印象深刻的成果,特别是对于 3.11 其性能较 3.10 显着提升。
随着社区/委员会的支持、多核处理器的日益普及以及 Faster CPython 的努力,GIL 采用计划第一阶段开始的时机已经成熟。
表演是什么样的?
我在我的两台机器(配备 Apple M3 Pro 的 MacBook Pro(CPU 有 6 个性能核心和 6 个效率核心))和安静的 EC2 实例(t3.2xlarge(8 个 vCPU))上运行了一些基准测试。
下图显示了使用和不使用 GIL 的 Python 3.12 和 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。亲自尝试一下!
- 我用过 超精细 运行基准测试,这是一个非常好的工具,但这些不是在专用硬件上运行的适当的科学基准测试。我的 MacBook 正在运行一大堆东西,甚至 EC2 实例也会在后台运行一些东西,尽管没有那么多。
- 这些基准测试确实很有趣,但请记住,在现实世界中,大多数执行 CPU 密集型工作的库都使用 赛通 或类似的——很少有人使用原始 Python 来执行计算密集型任务。 Cython 能够在执行期间暂时释放 GIL,并且这种情况已经存在了一段时间了。这些基准测试并不代表该用例。
考虑到这一点,我们已经可以做出一些观察:
- 当 Python 构建为支持自由线程时,性能下降非常明显——大约 20%。
- 是否通过以下命令重新启用 GIL 并不重要
-X gil=1
说法,性能下降是一样的。 - 正如预期的那样,禁用 GIL 后多线程显示出显着的提升。
- 正如预期的那样,启用 GIL 的多线程比单线程慢。
- 禁用 GIL 的多线程与多处理大致相同。话又说回来,这是一个非常简单的例子,您不需要做太多实际工作。
- Apple Silicon 芯片确实令人印象深刻。我的 M3 Pro 上的单线程性能比 t3.2xlarge 上的单线程性能快约 4 倍。我的意思是,我知道 t3 的设计目的是便宜且可爆,但即便如此!如果你考虑一下这些东西带来的疯狂电池寿命,那就更令人印象深刻了。
2024-09-30 更新:它们如何扩展?
我运行了一些额外的基准测试,以了解性能如何随着线程/进程的数量而变化。以下是以秒为单位的图表:
(不要问我的 MacBook 上的块 23 发生了什么,显然是某些东西决定在后台占用大量 CPU。)
正如预期的那样,更改线程数不会改变启用 GIL 的运行时的性能,而禁用 GIL 的运行时和多处理模式都会提供一条趋于最小执行时间的良好曲线,其中非并行部分和硬件限制(即物理核心的数量)限制了性能的提高。
令我有点惊讶的是,在多线程和多处理模式下,性能的持续改善远远超过了物理核心的数量。 M3有12个核心 不执行任何同步多线程 (SMT)3 而 t3.2xlarge 有 8 个 vCPU,实际上只是 4芯SMT 所以我不太明白你为什么在 16 个线程/进程下仍然看到比 15 个线程/进程更好的性能。如果你知道这是为什么,请留下评论或电子邮件!
当您将 is 绘制为加速分数时,这一点会变得更加清晰:
这显示了与之前相同的数据,但每个数据点都根据该运行时和模式的基本情况(线程/进程数为 1)的性能改进进行了缩放。
关心性能的人喜欢这样绘制它,以便他们可以将其与 阿姆达尔定律 这是通过并行化可以加速程序多少的理论限制,尽管这显然不是正确的性能分析,但主要只是为了好玩 😎📈
如何尝试自由线程 Python?
截至撰写本文时,Python 3.13 仍处于候选版本阶段,尚未正式发布。话虽如此,今天是28号星期六th 预计 9 月发布 2ND 7th 十月是 周三 周四一周,所以距离不远了。 (更新:更新发布日期以反映 推迟发布时间表.)
如果你想提前尝试一下,那你就不走运了 黑麦 这似乎只提供已部署的版本并且 紫外线 其中包括 3.13.0rc2 版本,但不包括 3.13.0rc2t 版本。幸运的是, pyenv 支持 3.13.0rc2 和 3.13.0rc2t。亲自尝试一下:
# If you're reading this from the future, rye may have it:
$ uv python list | rg -F cpython-3.13
# pyenv should have it, though.
$ pyenv install --list | rg '^s+3.13'
# Take 3.13.0rc2t for a spin
$ pyenv install 3.13.0rc2t
Python 3.13.0rc2 experimental free-threading build (main, Sep 18 2024, 16:41:38) [Clang 15.0.0 (clang-1500.3.9.4)]
$ python -c 'import sys;print("GIL enabled 🔒" if sys._is_gil_enabled() else "GIL disabled 😎")'
# GIL can be re-enabled at runtime
$ python -X gil=1 -c 'import sys;print("GIL enabled 🔒" if sys._is_gil_enabled() else "GIL disabled 😎")'
如果您正在尝试使用自由线程 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”,是一种编译技术,即及时生成机器代码来执行它,而不是像 gcc 或 clang 等传统 C 编译器那样提前 (AOT)。
我们之前已经讨论过字节码和解释器。重要的是,在 Python 3.13 之前,解释器会一次查看每一条字节码指令,并在执行之前将每一条指令转换为本机机器代码。随着 JIT 编译器的引入,现在可以将字节码“解释”为机器代码一次并根据需要进行更新,而不是每次都重新解释。
需要指出的是,这种 JIT 已经 3.13中引入 就是所谓的 “复制和修补”JIT。这是 2021 年一篇名为《 “复制和补丁编译:高级语言和字节码的快速编译算法。与更高级的 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-dev
python-build: use openssl@3 from homebrew
python-build: use readline from homebrew
Cloning https://github.com/python/cpython...
Installing Python-3.13-dev...
python-build: use tcl-tk from homebrew
python-build: use readline from homebrew
python-build: use ncurses from homebrew
python-build: use zlib from xcode sdk
Installed Python-3.13-dev to /Users/drew.silcock/.pyenv/versions/3.13-dev
$ python -c 'import sysconfig;print("JIT enabled 🚀" if "-D_Py_JIT" in sysconfig.get_config_var("PY_CORE_CFLAGS") else "JIT disabled 😒")'
您可以阅读其他配置选项 在 PEP 744 讨论页面上 (就像启用 JIT 但要求通过运行来启用它 -X jit=1
在运行时等)。
这里的测试仅检查 JIT 是否在构建时启用,而不检查它当前是否正在运行(例如已在运行时禁用)。可以在运行时检查 JIT 是否已启用,但这有点棘手。这是一个可以用来解决这个问题的脚本(取自 PEP 744 讨论页面)4:
def is_jitted(f: types.FunctionType) -> bool:
for i in range(0, len(f.__code__.co_code), 2):
_opcode.get_executor(f.__code__, i)
# This isn't a JIT build:
print("JIT enabled 🚀")
print("Doesn't look like the JIT is enabled 🥱")
if __name__ == "__main__":
PEP 744 讨论提到了两者 PYTHON_JIT=0/1
和 -X jit=0/1
– 我没有发现 -X
选项确实做了任何事情,但环境变量似乎可以解决问题。
$ PYTHON_JIT=0 python is-jit.py
Doesn't look like the JIT is enabled 🥱
结论
Python 3.13 是一个重大版本,向运行时引入了一些令人兴奋的新概念和功能。它不太可能与您编写和运行 Python 的方式产生任何直接的不同,但很可能在接下来的几个月和几年里,随着自由线程和 JIT 变得更加成熟和完善,它们将开始拥有越来越多的功能对 Python 代码性能的影响,尤其是 CPU 密集型任务。
进一步阅读
更新
- 2024-09-28:将 v3.13.0 的发布日期从 2 更新为ND 10月至7日th 十月。由于 恩斯托克 HN 指出了这一点。
- 2024-09-30:更新了图表以使其更具可读性,并添加了有关性能扩展的额外部分。