作者:Łukasz Langa
译者:豌豆花下猫,起源:Python 猫
原文:https://lukasz.langa.pl/5d044…
在一年一度的 Python 外围开发者 sprint 会议期间,咱们与 Sam Gross 举办了一次会议,他是 nogil 的作者。nogil 是 Python 3.9 的分叉版本,移除了 GIL。这是一份非正式的会议纪要。
简略总结
Sam 的工作证实了以他的形式删除 GIL 是可行的,即生成的 Python 解释器的性能良好,并且能够随着 CPU 内核的减少而扩大。为了最终达到侧面的成果,还须要有其它看似无关的解释器工作。
目前还不可能将 Sam 的更改合并到 CPython,因为他的更改是针对 3.9 分支进行的,便于用户拿以后 pip 可装置的库和 C 扩大对 nogil 解释器进行测试。如果要合并 nogil,就不得不基于 main 分支进行更改(目前 main 分支已布局为 3.11)。
不要指望 Python 3.11 会移除 GIL。 将 Sam 的工作合并到 CPython 自身将是一个艰辛的过程,但这仅仅是所需的一部分:在 CPython 移除 GIL 之前,须要为社区制订一个良好的向后兼容的迁徙打算。这些都还没有打算好,所以咱们认为机会还没到。
有些人在议论如此微小的变动时提到了 Python 4。外围开发人员以后没有打算公布 Python 4,事实上恰恰相反:咱们正踊跃地防止公布 Python 4,因为 Python 2 到 3 的转换对社区来说曾经足够艰难了。当初思考或者放心 Python 4,必定还为时过早。
介绍 nogil
Sam 公布了他的代码,同时还有一篇具体的文章,解释了该项目标动机和设计。
nogil 代码地址:https://github.com/colesbury/…
他的设计能够总结为:
- 为了线程平安,将 Python 内置的分配器
pymalloc
替换成mimalloc
,对字典和其它汇合对象采纳无锁读写,同时晋升效率(堆内存布局容许在不保护显式列表的状况下找到 GC 跟踪的对象) -
用有偏见的援用计数(biased reference counting)代替非原子的急迫的援用计数(non-atomic eager reference counting):
- 将每个对象与创立它的线程(称为 owner thread)绑定;
- 对象在 owner thread 内应用时,采纳疾速的非原子的部分型援用计数;
- 对象在其它线程内应用时,采纳较慢的但原子的共享型援用计数;
-
为了放慢跨线程的对象拜访(因为会被原子的共享型援用计数拖慢),引入两种技术:
- 有些非凡对象是永生的,这意味着它们的援用计数永远不会被计算,也永远不会被开释:这蕴含像 None、True、False 这样的单例对象,小整数和常驻的字符串,以及动态调配的内置类型 PyTypeObjects;
- 其它全局可拜访对象应用提早援用计数(deferred reference counting),如顶级的函数、代码对象和模块;它们不是永生的,并不总是在程序的生命周期内存活;
-
调整循环的垃圾回收器成一个单线程的 stop-the-world 垃圾回收器:
- 期待所有线程在一个平安点(任何字节码的边界)挂起;
- 不期待阻塞在 I/O 的线程(应用
PyEval_ReleaseThread
,相当于在以后 Python 中开释 GIL); - 高效地结构对象的列表,以便即时地开释:得益于
mimalloc
,GC 跟踪的对象都保留在一个独自的轻量级的堆中;
- 将全局过程的 MRO 缓存迁徙到部分线程里,防止查找 MRO 时的争用;缓存生效依然是全局性的;
- 批改内置的汇合类对象,使之成为线程平安的。
Sam 的设计文档蕴含了这些设计元素的细节,蕴含线程状态与 GIL API 的信息,以及解释器和字节码的其它批改(用带有累加器的寄存器 VM 替换堆栈 VM;通过防止创立 C 语言的栈帧来优化函数调用;ceval.c 的其它变更;标签指针的应用;LOAD_ATTR、LOAD_METHOD、LOAD_GLOBAL 操作码的线程平安的元数据;等等)。我倡议你残缺地浏览它。
Python 猫注:上文呈现的“stop-the-world”,有时缩写成“STW”,这是少数垃圾回收器的工作机制,示意在垃圾回收器工作时,其它线程全副临时挂起,从而保障援用对象的精确更新,其毛病是对程序性能有所影响;“MRO”是“method resolution order”的缩写,即“类办法解析程序”,示意在所有基类中搜寻成员办法时的秩序。
晚期的基准测试
在 pyperformance 基准测试套上,作为概念验证的 nogil 解释器比 3.9 快 10%。据估计,在解释器的全副批改中,移除 GIL 会导致性能变慢 9%,次要是因为有偏见的援用计数和提早援用计数。换句话说,Python 3.9 加上 nogil 的所有更改,但不移除 GIL 自身,能够快 19%。然而,这样并不能解决多核的可伸缩性问题。
顺便说一下,nogil 的一些更改,比方将 C 调用栈与 Python 调用栈解耦,曾经在 Python 3.11 中实现了。事实上,咱们有针对以后 main 分支的初步的基准测试,结果表明在单线程的性能上,Python 3.11 比 nogil 快 16%。
须要有更多的基准测试,特地是应用 Larry Hastings 在对 Gilectomy 进行测试时应用的基准测试(过后基于 Python 3.5,起初移植到 3.6 alpha 1)。
Python 猫注:gilectomy 是由 GIL ectomy 两个单词组合而成,ectomy 是一个医学上的术语“切除术”,可见这个我的项目的用意跟 nogil 是一样的!这是 5-6 年前的我的项目,作者曾在 PyCon 大会上做过几次分享。但这个我的项目反而导致 Python 总体性能降落了,最初无疾而终。
gilectomy 我的项目作者在 PyCon 上的分享:
2015 年分享:https://www.youtube.com/watch…
2016 年分享:https://www.youtube.com/watch…
2017 年分享:https://www.youtube.com/watch…
Sam 揭示咱们,一个用户程序在无 GIL 的 Python 上的伸缩性实际上取决于最终的代码。如果不进行测试,就不可能预测代码在没有 GIL 的状况下体现如何。因而,如果提供一个繁多的数字来阐明无 GIL 的 Python 速度会晋升 x 倍,这是不负责任的。
会议中向 Sam 提出的问题
为了清晰易懂,这里的问题基于会议上的内容进行了从新排序。答案是由 Sam 的答复转述而来的,并失去了他浏览草稿后的认可。要留神的是,外围团队的成员可能对其中一些主题有其它观点。
Q:有哪些可感知的危险是妨碍 nogil 我的项目合入到 CPython 中的?
目前的代码库曾经证实了它在技术上的可行性。它能够运行,而且比一般的 CPython 解释器和 Gilectomy 我的项目更具备可伸缩性和好性能。我在该我的项目中投入了将近两年的全职工作。
这齐全取决于社区对 C 扩大程序的革新水平,以确保它们不会导致解释器彻底解体。而后,剩下的长尾就是社区要以一种既正确又可扩大的形式在应用程序中采纳自在线程。这两个是最大的挑战,但咱们必须乐观应答。
Q:你打算如何改良你的工作?对 commit 秩序有什么倡议吗?你将如何放弃你的工作与 main 分支的同步?
Sam 目前正在重构他的工作,最后是基于 3.9.0a3,将匹配 3.9.7 最终版本。这项工作的一部分是将 commit 重构为逻辑单元,以便更好地阐明哪些内容须要更改(哪些地方改了,以及为什么要改)。
目前还不打算把这项工作移到 main 分支(将来的 3.11),因为这个分支太不稳固了。相比之下,3.9 有大量已公布的可通过 pip 装置的库和 C 扩大,可用于测试。这使得 Sam 可能评估该我的项目与真实世界的第三方代码的行为。基于 main 的批改将破费不少工夫,而这些工夫本能够花在改良无 GIL 的解释器上,所以,当初就基于主分支的话,还为时过早。
将工作进行宰割而后再合并是可行的,但必须记住,许多更新须要在串联起来时,性能才会晋升。独自而言,它们会导致(临时的?)性能降落。
外围开发者注:咱们当初不能合并对 3.9 分支所做的更改。在我的项目的这个阶段应用 3.9 是有意义的,但要害的是要将它宰割成可生产的数据块,而后一个一个地合并到 main 分支中。一块一块地做,很有可能会侵害性能,但这是惟一事实的集成路径。
Q:能够只引入寄存器 VM 和编译器而不做其它更改吗?在不扭转援用计数或 GIL 的状况下应用寄存器 VM 会有什么非凡的艰难吗?
VM 应用提早 / 永生的援用计数。能够将其转换为只应用经典的援用计数,但最终后果的效率还不分明(例如,出于性能思考,堆栈上的所有对象都应用了提早援用计数)。
Q:跟前一问相同的问题:只引入 nogil,而不应用新的寄存器 VM,会有什么艰难呢?
尽管新的 VM 只进步了性能,而不是准确性,但它也进步了可伸缩性,使得无 GIL 的 Python 能够充分利用 CPU 内核而不产生争用。因而要应用 3.11 解释器也是可行的,但最好保留一些寄存器 VM 的设计思维,这对可伸缩性和线程平安很重要。这须要做大量的工作。然而将寄存器 VM 更新成跟 main 分支一样(以及修复遗留的 bug),也须要大量的工作。这两种抉择都是可行的。
Q:对于那些不心愿本人的代码被其它线程并行运行的 C 扩大,有什么倡议么?在适应新的自在线程环境之前,难道不须要 CPython 给它们提供一些 API 来补救差距吗?
这须要花工夫。指标是渐进式驳回,最终推广至大多数 C 扩大。GIL 能够作为解释器启动时的一个选项。如果没有启用 GIL,并且 C 扩大不反对新的操作模式,可能就要产生告警或者不让其导入。Python 社区不得不适配 C 扩大,让它们适应无 GIL 的模式。
作为概念验证的 nogil 我的项目,默认应用无 GIL 模式,并承受任何 C 扩大。如果它被 CPython 采纳了,那么在开始时默认应该启用 GIL(要求在启动 Python 时应用 -X nogil
禁用 GIL),以便让第三方库做适配。而后,在公布几个版本后,默认值再切换成无 GIL 的模式。
尽管要移植全副货色并不容易(并行是很难的),但在少数状况下,移植并不会很难,特地是对于封装内部库的 C 扩大来说。
外围开发者注:有大量的“暗物质”Python 代码(和 C 扩大)不是开源的。咱们须要小心不去毁坏它们,因为它们的用户可能无奈做出所需的更改,或者向上游报告问题给咱们。特地地,有些 C 扩大应用 GIL 来爱护它们本人的外部状态。这是一个很大的担心,可能是采纳无 GIL Python 的一个很大的阻碍。
Q:你会增加一个 PEP-489 的“插槽”么,以便 C 扩大用来示意其反对 nogil,这样当遇到不反对 nogil 的库时,就不让它导入?
很多人也提过,这可能是一个好主见,但我不齐全分明这意味着什么。抉择无 GIL 模式并不能保障没有 bug。相同,在默认状况下,咱们运行所有的扩大(当初的 nogil 就是这么做的)。不兼容的扩大能够应用 PyInit 模块的代码,被动地询问解释器是否启用了 GIL,如果不兼容的话,就在导入时产生正告甚至异样。
Q:在运行期启用 nogil 是一项长期可行的抉择,还是过渡性的性能呢?
现实的终局是 CPython 不再有 GIL,句号。然而,预计将有一个漫长的社区适应期。咱们心愿防止从 Python2 到 Python3 过渡时的断裂。精确地说,咱们心愿过渡得越平滑越好,即便这意味着须要延展更长的工夫。
Q: 确认一下,最终状态是只有 nogil,并且不反对再开启 GIL 么?
目前咱们还不确定。现实的终局是只存在一个无 GIL 的 Python,但尚不分明这是否实现。
Q:如果这些个性标记会继续很长一段时间,这是否意味着咱们须要大幅减少测试矩阵?
是的,测试矩阵须要加倍。然而,测试无 GIL 版本可能是判断经典的 GIL 版本是否无效的一个很好的预测器。有必要偶然(每晚?)运行启用了 GIL 的测试。
外围开发者注:如果不做测试,代码将减速进化。在 CPython 中,因为须要运行工夫(例如测试援用透露时),咱们不会在每次更改时都运行所有测试,但如果有更改导致每日测试失败,咱们会立刻回退更改,因为在曾经失败的构建点之后,很可能会呈现其它的回归问题。
Q:你认为多个 Python 解释器并行运行,每个解释器一个 GIL 怎么样?
Python 猫注:给大家科普一下这个问题的背景,PEP-554 提议实现多解释器来解决 GIL 的问题。这是在 2017 年提出的,受到挺多关注。在 2019 年时,我曾翻译过《Has the Python GIL been slain?》介绍它。然而,目前该提案仍然是草稿状态,具体的开发状况不甚明朗。
跟无 GIL 提案相比,这既是互补的,又是相互竞争的。在无 GIL 解释器中也能够反对副解释器。
目前还不分明多解释器计划是否实现。有了 nogil,就不须要放心跨线程共享对象,也不须要放心 C 扩大的兼容性,因为有了多解释器,就没有任何状态是真正全局的,因而须要特地地隔离。对于可变对象,在多解释器之间传递时,须要某种模式的序列化 / 反序列化。对于不可变对象,解释器可能会增加非凡的反对,但如果它们不是已知的不可变的内置类型,用户代码就须要适配这些对象。这是从 PyTorch 的相干工作中失去的启发,它应用了某种模式的多解释器。
因为我最感兴趣的用例实际上是迷信数据(PyTorch 训练工作流),间接而无效地共享数据的能力对多线程性能至关重要。如果采纳多解释器,这种共享只能在 C 扩大级别上开启,与无 GIL 的 Python 相比,将导致更多应用 C/C++ 代码。
Q:你曾经具体介绍了字典和列表的实现。其它可变类型例如队列、汇合、数组等等,是如何实现的呢?
nogil 是一个开发中的我的项目。因为字典和列表在解释器的外部运作中很广泛,所以它们的开发最多。同样地,队列的开发曾经实现,但其它类型还没有。汇合是下一个要笼罩的重要内容。
队列十分重要,因为它被concurrent.futures
和asyncio
用于并发线程之间的通信。队列比字典和列表简略,它应用细粒度的锁而不是无锁读取。其它的对象很可能须要组合应用。
这项工作很辣手,因为在获取和开释锁时须要小心,例如 Py_DECREFs 是可重入的。还能够思考应用更“粗粒度”的锁,但当然了,这些锁都有死锁的危险。
Q:nogil 有多依赖 mimalloc? 如果咱们把它作为一个编译期选项,能够用或不必它,那么应用平台的 malloc 来代替没有 C 预处理器天堂的低性能构建是否可行?
mimalloc 不仅仅是用于线程平安。它对于启用字典的无锁读取是必要的,还反对高效的 GC 追踪。
mimalloc 的维护者对显式地反对 CPython 很感兴趣,并且乐意为实现这一点进行必要的更改。
其它实现的 malloc 据说也稳固反对 CPython:在 Facebook 中应用的jemalloc
,在谷歌中应用tcmalloc
,只管集成得较少,更像是默认分配器的简略替换。(Python 猫注:前文提到的 mimalloc 是微软的)
外围开发者注:Christian Heimes 和 Pablo Galindo Salgado 正在评估 CPython 应用 mimalloc。晚期测试在均匀上(几何平均数)没有性能消退,大多数基准测试做得更好,多数基准测试做得略微差一些。还有一些待评估的问题:
- mimalloc 的 API 和 ABI 的稳定性;
- 受权许可;
- 跨所有 CPython 反对的平台的可移植性,例如 stdatomic.h 仅在 C11 中可用;
- 集成剖析和检测工具(Valgrind、asan、ubsan 等等);
-
可能还有其它。
Q:你的我的项目和 Larry 的 Gilectomy 有什么相似之处?你能利用他的我的项目吗?
在顶层设计上,两个我的项目是类似的:提早援用计数,细粒度锁,对于返回借用的援用的挑战。没有复用 Gilectomy 的代码。
Q:你说你的我的项目在顶层上相似于 Larry 的 Gilectomy。他的我的项目也是基于提早援用计数。然而,他在 Gilectomy 上只失去了性能降落的后果,而你的“nogil”却有很好的性能体现。你认为这种差别是怎么回事?
切换到基于寄存器的编译器和其它优化,比方由 mimalloc 提供的无锁的字典读取,以及应用提早援用计数来防止争用,对 nogil 的扩展性和性能都至关重要。而且,在某些状况下,Python 自身变得更快了。例如,Python 3.9 中的函数调用比 Python 3.5 的要快得多。
让它反对扩大,必定比预期要花更多的工作。
Q:有没有可能在无 GIL 模式中退出一个(不兼容的)C 扩大或剔除它吗?
顾名思义,GIL 就是一个全局锁。为了爱护任意一段共享数据,它须要在所有线程上开启,包含不兼容的扩大所处的线程。
在曾经运行的过程中,将无 GIL 的解释器切换为应用 GIL 的解释器是很辣手的(反之亦然)。最好的做法是在启动时抉择:要么在过程中启用 GIL,要么不启用。如果 C 扩大没有标记为兼容,就引发正告或无奈导入。
或者,当拜访 C 扩大时,也能够“stop the world”,但这与移除 GIL 而所想达成的目标不符。
外围开发者注:到目前为止,还有其它的想法须要深入探讨。有种想法是将 GIL 转换为“单写多读”锁。在这种状况下,无 GIL 的模式将获取“多读”锁,也就是说,不会阻塞其它新代码做同样的事件。而历史遗留的代码将取得一个“单写”锁,阻塞其它所有线程执行,直到锁开释。这种设计须要保留获取 / 开释 GIL 的 api,nogil 曾经这样做了,为了告知 GC 一个线程被阻塞在 I/O 上。
Q:有没有可能将函数标记为非线程平安的(比方应用装璜器),并让 nogil 在运行代码时加锁,以避免其它线程调用它?(有点像长期的 GIL)
如果放心的是状态被其它线程拜访,则须要锁定每一次拜访。这在装璜器层面上不是特地可行。正如之前说过,条件性地为不平安的代码开启 GIL 是很难实现的。
Q:用你本人的锁代替 GIL 会很艰难。应用 nogil,你认为与线程相干的问题会减少么?
不分明。对于 C API 扩大,至多有一种好的设计模式:它们通常有相似的构造,并在单个构造中放弃共享状态。目前,Pybind11 看起来与这个模式间隔最远,因而用它编写的 C 扩大可能须要进行大量更改。
许多简单的 C 扩大曾经不得不解决锁和多线程,因为它们的目标是尽可能多地开释 GIL,比方 numpy。所以,兴许令人诧异的是,那些我的项目可能更容易迁徙。
下一步工作
在这次会议之后,外围开发者们探讨了将 nogil 纳入主我的项目的可行性,以及这对社区意味着什么。毫无疑问,这种水平的扭转必须十分小心。
在作出决定之前,咱们感觉先引入它的一些代码更为可行。特地地,mimalloc 看起来很乏味,曾经有一个 open 的 pull 申请了(https://github.com/python/cpy…),旨在摸索引入它。在那里能够找到基准测试的链接。
在集体层面上,咱们对 Sam 所做的工作印象粗浅,并邀请他退出 CPython 我的项目。我很快乐地通知大家,他对此很感兴趣,为了帮忙他成为一名外围开发者,我将为他提供领导。Guido 和 Neil Schemenauer 将帮我检视我不相熟的解释器局部的代码。