Skip to content

필사 모드: Python 3.15 的 abi3t — free-threaded 构建的 stable ABI 解决了什么问题,代价又是什么

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — 摘掉 GIL 之后剩下的问题

free-threaded Python 的大局故事看起来已经尘埃落定。随着 PEP 779 在 2025 年 6 月 16 日敲定,free-threaded 构建从 Python 3.14 起脱离了实验阶段。用 PEP 的术语来说,这就是Phase II — 得到官方支持,但仍是可选构建。而Phase III 则是让 free-threaded 成为默认构建的阶段。

于是自然会问「什么时候会成为默认」。而挡住这个答案的,并不只是解释器性能。

看 PEP 779 的 Phase II 标准,其中有一项性能指标 — 对单线程开销设定了「15% as a hard performance target」,在撰写该 PEP 时的实测数字大约在 10% 左右。内存方面,目标是基于 pyperformance 几何平均达到 20%。到这里为止,都还是解释器内部的问题。

然而,同一份 PEP 的 Open Issues 一节里列出了 stable ABI — 不是作为已确定的需求,而是作为一个悬而未决的问题。而这个悬而未决的问题,恰恰是在性能数字之前就已经卡住整个生态系统的实质瓶颈。本文要讲的,正是这个缺口如何被 PEP 803 填补,以及代价是什么。

为什么一个 .so 无法同时服务两种构建

先说根本原因。free-threaded 构建不只是单纯去掉 GIL,它还改变了对象本身的形态。PEP 793 的表述很精确 — 常规构建与 free-threading 构建中,Python 对象的内存布局是不同的。这是因为引用计数方式变了,对象头里多了字段。

布局不同会导致什么被破坏?在编译时静态分配的 Python 对象会被破坏。C 扩展写死在源代码里的结构体正属于这一类。PEP 793 指出,模块定义是其中最常见的绊脚石 — 让一个编译好的库文件无法同时在常规构建和 free-threaded 构建上使用的最常见障碍,正是 PyModuleDefPyModuleDef 内部含有一个 PyModuleDef_Base,而后者又含有对象头。如果头部大小在不同构建间不一样,那么静态嵌入了这个结构体的 .so,就只对其中一种构建有效。

于是 ABI 分裂了。cp314cp314t 是两种不同的 ABI,扩展必须分别构建。

Wheel 矩阵 — PEP 803 实际引用的数字

这里得回想一下 stable ABI(abi3)存在的理由。abi3 是让你「不必为每个 Python 小版本重新构建」的机制 — 它把版本这条轴折叠了起来。构建一次,就能在 3.x 及以上版本持续加载。

问题是,这在 free-threaded 构建上根本行不通。用 PEP 803 原文的说法 — stable ABI 无法在 free-threaded 构建上使用,如果 Py_LIMITED_APIPy_GIL_DISABLED 这两个宏同时被定义,扩展就会构建失败。

也就是说,一旦走向 free-threading,那条已经折叠起来的版本轴就会重新展开。那 wheel 数量会变成什么样呢 — PEP 803 的 Motivation 部分没有用抽象的担忧,而是引用了实际维护者的数字。

  • cryptography — 在最近一个版本里发布了 48 个 wheel 文件。维护者 Alex Gaynor 的原话:"As long as we have O(1) builds, that's ok. What we can't/won't do is O(n) where we need new builds for every Python release."
  • SciPy — 在上一个版本里为了支持 4 个 CPython 版本,上传了 60 个按版本区分的 wheel。(SciPy 指导委员会主席 Ralf Gommers)
  • pydantic-core — 在最新版本里发布了 112 个 wheel,而且这个数字还会继续增长。维护者 David Hewitt 的警告一针见血:"if free-threading does not adopt a stable ABI, all the benefits above will be lost when free-threading becomes the default."

最后这句话与 Phase III 直接相关。一旦 free-threading 成为默认,如果没有 stable ABI,此前靠 abi3 获得的所有好处会一次性蒸发。所以指导委员会的立场是,用于 free-threading 的 stable ABI 必须赶在 Python 3.15 之前准备好并定义清楚。PEP 803 就是对这一诉求的回应。

abi3t 做的事

PEP 803 — "abi3t": Stable ABI for Free-Threaded Builds。作者是 Petr Viktorin 和 Nathan Goldbaum,于 2026 年 3 月 30 日被 Accepted,目标是 Python 3.15。

核心不是修改 abi3,而是再造一个变体。扩展在编译时定义的是 Py_TARGET_ABI3T,而不是(准确地说,是在此之外再加上)Py_LIMITED_API

/* 在 include Python.h 之前 */
#define Py_TARGET_ABI3T 0x30f0000   /* 3.15 */

/* 或者用编译器标志 */
/* -DPy_TARGET_ABI3T=0x30f0000 */

而这里是实务上最重要的一点。用 abi3t 编译出的扩展,并不是 free-threaded 构建专属的。用 PEP 摘要里的原话来说 — 实务上,abi3t 3.15 与 abi3 3.15 是兼容的。PEP 建议扩展作者显式地一次性为两种 ABI 编译,并用 wheel 标签 abi3.abi3t 来标示这种兼容性。

归纳一下,就是这样:

现在 (到 3.14 为止)
  Python 版本 × 线程模式 × 平台  →  wheel 爆炸
  cp313-*, cp313t-*, cp314-*, cp314t-*, ... 每个平台都要重复一遍

abi3t 之后 (3.15+)
  一个二进制文件同时覆盖 3.15 及以上的 GIL 构建和 free-threaded 构建
  myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl
  → 版本轴和线程模式轴同时被折叠。剩下的只有平台轴。

文件名标签也变了 — 在类 Unix 系统上,用 .abi3t.so 取代 .abi3.so

配套的运行时保护机制也一并到来。在 free-threaded 构建上,PyModuleDef_Init() 会检测出使用了非 free-threading stable ABI 的扩展,在其被加载时给出提示信息并抛出异常。它选择的是响亮地拒绝,而不是悄无声息地崩溃。

代价 — abi3t 比 abi3 更窄

从这里开始要诚实面对了。abi3t 不是白送的升级,而是一个更严格的 ABI。因为吸收布局差异的唯一办法,就是「干脆不暴露布局」。

结构体变得不透明。Py_TARGET_ABI3T 编译后,PyObjectPyVarObjectPyModuleDef_BasePyModuleDef 都变成了不完整类型。无法直接访问字段,不知道大小和对齐方式,也不能把它们嵌入到别的结构体里。

因此以下这些宏消失了 — PyObject_HEAD_PyObject_EXTRA_INITPyObject_HEAD_INITPyObject_VAR_HEADPy_SET_TYPE()

写过 C 扩展的人看到这份名单里的 PyObject_HEAD,一定会愣一下 — 定义自定义类型的标准写法整个作废了。

/* 旧写法 — 在 abi3t 下不可行 */
typedef struct {
    PyObject_HEAD      /* 大小未知,禁止使用 */
    int my_data;
} CustomObject;

/* abi3t 写法 — 去掉基类,只定义"额外字段" */
typedef struct {
    int my_data;
} CustomObjectData;

static PyType_Spec CustomType_spec = {
    .basicsize = -sizeof(CustomObjectData),   /* 负的 basicsize (PEP 697) */
    /* ... */
};

要访问数据得用 PyObject_GetTypeData(),这里有一个陷阱 — 第二个参数必须是定义该类型的那个类,而不是 Py_TYPE(obj)。一旦涉及子类化,两者就会不一致。所以实例方法需要通过 METH_METHOD 标志和 PyCMethod 签名拿到 defining_class,或者使用类型令牌(type token)。

模块入口点也得换。 要用 PEP 793PyModExport_* 钩子取代 PyInit_* 钩子 — 既然 PyModuleDef 不能静态分配,这是自然的结果。PyModule_GetDef()PyType_GetModuleByDef() 也得换成基于模块令牌的 PyModule_GetToken() / PyType_GetModuleByToken()

还有一些东西根本无法移植。 可变大小类型 — 也就是使用 Py_tp_itemsizetp_itemsize 的类型 — 无法移植到 abi3t 3.15。这不是绕不绕得过去的问题,就是不行。

前提条件也不轻松。要以 abi3t 为目标,你必须已经支持 abi3,必须已经在用多阶段初始化(PyModuleDef_Slot),而且模块要么是隔离的,要么每个进程只加载一次。

编译期条件判断也得整理。PY_VERSION_HEXPY_MAJOR_VERSIONPY_MINOR_VERSION 反映的是构建时的版本,必须换成运行时值 Py_Version;而 Py_GIL_DISABLED 在 abi3t 下始终被定义,所以拿它做分支条件已经没有意义。

最重要的误解 — 能加载和线程安全是两回事

如果本文只带走一句话,那应该是这一句。这是 PEP 803 正文里的一句话。

"Implementing this PEP will make it possible to build extensions that can be successfully loaded on free-threaded Python, but not necessarily ones that are thread-safe without a GIL."

abi3t 解决的是分发问题,不是并发问题。你的扩展能用 abi3t 干净地构建出来、也能顺利加载到 free-threaded 解释器里,并不意味着那些原本无锁访问全局状态的代码突然就变安全了。真正的线程安全 API,是 PEP 明确留给后续工作的领域。

这个区分在运行时行为上也有体现。free-threaded 构建可以重新打开 GIL — 通过环境变量 PYTHON_GIL-X gil 选项。而根据官方文档,导入一个没有明确标注支持 free-threading 的 C 扩展,可能会自动重新打开 GIL,此时会打印出警告。

也就是说,「能加载」「能在没有 GIL 的情况下运行」「在没有 GIL 的情况下更快」,是三道彼此独立的关卡。abi3t 只是让第一道关卡可以被批量通过。

搭不上 3.14 的车 — 以及什么时候不该用

abi3t 没有回溯支持。用 PEP 的表述来说,以 abi3t 为目标的扩展,无论是在源码层面(API)还是编译产物层面(ABI),都不向后兼容更早的 CPython 版本。原因是它必须避开 PyModuleDef,改用新的 PyModExport 钩子。PEP 明确指出,它不提供针对 free-threaded 3.14 及更早版本的做法(cp314-abi3t),并把这个问题留给外部工具解决。

实务上,这意味着:如果你还得继续支持 3.10 到 3.14,那个区间原有的 wheel 矩阵会原样保留。abi3t 带来的缓解是从 3.15 起步、逐年累积的那种,不是明年 CI 时间就砍半的那种。

而且也不是所有人都需要迁移。 3.15 的 What's New 文档把这一点说得很清楚 — stable ABI 并不提供 CPython 的全部功能,无法迁移到 abi3t 的扩展,可以继续沿用老办法,分别构建现有的 stable ABI(abi3)和面向 free-threading 的按版本区分的 ABI(cp315t)。

归纳起来,可以这样划分。

abi3t 值回票价的情况

  • 你发布的 wheel 已经是数十个的规模,而且每增加一个 Python 版本就线性增长。
  • 已经支持 abi3,并且在用多阶段初始化 — 前提条件已经满足了一半以上。
  • 真的有计划要支持 free-threaded 用户。

属于过度或不可行的情况

  • 这是一个纯 Python 包。正如 free-threading 指南所说,纯 Python 代码按设计本就无需修改即可工作 — 没什么可做的。
  • 用了可变大小类型,或者需要 stable ABI 之外的 API — 这本来就不是候选对象。
  • 扩展仅供内部使用,且只支持一个 Python 版本 — 没有轴可以折叠,剩下的只有迁移成本。

那么,Phase III 是什么时候

先诚实地摆数字。根据官方文档,在 pyperformance 基准测试套件上,单线程开销平均为 macOS aarch64 上约 1%,x86-64 Linux 上约 8%。文档同时附带说明,这会因工作负载和硬件而异。

再看走势。在 PyCon US 2026(5 月中旬,长滩)上,CPython 核心开发者、指导委员会成员 Thomas Wouters 做了一场演讲,LWN 对此做了总结。在 3.13 中,单线程工作负载会慢 20%~40%;到了 3.14,这个数字降到了 0%~10%。Wouters 本人也说,在 ARM 上做到 0% 减速让他"personally shocked",而在 Linux 的 GCC 上通常在 5% 左右,最多可能到 10%。

生态系统这边,按 LWN 的说法,PyPI 下载量前 360 名的二进制 wheel 中,有超过 50% 已经支持 free-threading,主要归功于 Meta 和 Quansight 的工作。(这个「前 360」的样本口径,和 hugovk 的追踪器所追踪的「下载量前 360 名且带扩展的包」是同一个标准。)

即便如此,Phase III 既没有 PEP,也没有时间表。PEP 779 已经把这一点说明白了 — 进入 Phase II 的条件和 Phase III 的决定,是完全不同的两回事,后者将主要取决于社区的支持程度。要公平引用 Wouters 的预测,就必须连同他自己加的保留条件一起引用。他说自己相信 free-threaded 版本迟早会成为默认的 Python,但同时也钉死了一句 "I'm not saying this with any authority whatsoever"。他个人的猜测是在 3.16(2027 年 10 月)之后、3.20(2031 年 10 月)之前 — 也就是说,即便按最乐观的估计,至少也还有一年以上,而区间跨度是四年。

剩下的功课也很明确。Wouters 说,解释器和第三方包两侧都还需要进一步的性能与可扩展性改进,并且指出跨线程共享对象仍然是一个性能问题。这完全不是 abi3t 所触及的领域。

补充一点时间线上的感觉,按 PEP 790,Python 3.15 已于 2026 年 5 月 7 日的 beta 1 完成功能冻结,rc1 是 2026 年 8 月 4 日,rc2 是 9 月 1 日,最终发布是 2026 年 10 月 1 日。写这篇文章的此刻(7 月中旬)正处于 beta 阶段,官方文档也是基于 3.15.0b3 构建的。也就是说,abi3t 是已经定案的设计,但还不是已经发布的东西。

结语

关于 free-threading 的故事,通常从「GIL 消失了」开始,以一张基准测试图表结束。但在 3.14 通过 Phase II 之后,真正需要推进的地方并不是解释器,而是分发流水线 — abi3 曾经折叠起来的版本轴,在 free-threading 面前重新展开的问题,也就是 cryptography 的 48 个、SciPy 的 60 个、pydantic-core 的 112 个 wheel 矩阵翻倍的问题。

PEP 803 的 abi3t 把这条轴重新折叠了回去。一个二进制文件同时服务于 3.15 及以上的 GIL 构建和 free-threaded 构建,标签是 abi3.abi3t。代价是一个更窄的 ABI — 变得不透明的 PyObject、消失的 PyObject_HEAD、负的 basicsize、PyModExport 钩子,以及无法移植的可变大小类型。

而且必须精确知道它解决的问题的边界在哪里。abi3t 只是让你的扩展能够加载到 free-threaded Python 上,并不会让它线程安全。那仍然是你自己的活儿,PEP 自己也是这么写的。

如果你在等 Phase III,可以这样归纳 — 性能已经接近目标(视架构而定为 1%~8%),生态系统已经越过前 360 名中的一半,现在连 ABI 的缺口也在 3.15 被补上了。可即便如此,转向默认构建这件事,连 PEP 都还没有。剩下的不是技术问题,而是共识问题。而共识的移动速度,比基准测试慢得多。

参考资料

현재 단락 (1/88)

free-threaded Python 的大局故事看起来已经尘埃落定。随着 [PEP 779](https://peps.python.org/pep-0779/) 在 2025 年 6 月 16 ...

작성 글자: 0원문 글자: 8,474작성 단락: 0/88