Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 590 – Vectorcall:CPython 的快速调用协议

作者:
Mark Shannon <mark at hotpy.org>,Jeroen Demeyer <J.Demeyer at UGent.be>
BDFL 代理:
Petr Viktorin <encukou at gmail.com>
状态:
已接受
类型:
标准轨道
创建:
2019 年 3 月 29 日
Python 版本:
3.8
发布历史:


目录

重要

本 PEP 是历史文件。最新、规范的文档现在可以在 Vectorcall 协议 中找到。

×

请参阅 PEP 1,了解如何提出更改建议。

摘要

本 PEP 引入了一个新的 C API 来优化对象调用。它引入了一个新的 “vectorcall” 协议和调用约定。这基于 “fastcall” 约定,该约定已由 CPython 内部使用。任何用户定义的扩展类都可以使用这些新功能。

大多数新的 API 在 CPython 3.8 中是私有的。计划是最终确定语义并在 Python 3.9 中将其公开。

注意:本 PEP 仅涉及 Python/C API,它不会影响 Python 语言或标准库。

动机

调用约定的选择会影响调用两侧代码的性能和灵活性。通常,性能和灵活性之间存在矛盾。

当前的 tp_call [2] 调用约定足够灵活,可以涵盖所有情况,但其性能很差。性能低下主要是由于在调用过程中必须创建中间元组,以及可能创建中间字典。这在 CPython 中通过包含特殊情况代码来加速对 Python 和内置函数的调用来缓解。不幸的是,这意味着其他可调用对象(如类和第三方扩展对象)使用较慢、更通用的 tp_call 调用约定进行调用。

本 PEP 建议将用于 Python 和内置函数的内部调用约定推广和发布,以便所有调用都可以从更好的性能中受益。新提出的调用约定并不完全通用,但涵盖了绝大多数调用。它旨在消除临时对象创建和多个间接引用的开销。

The tp_call 约定的另一个效率低下之处在于,它每个类只有一个函数指针,而不是每个对象一个。这对于对类的调用效率低下,因为需要创建几个中间对象。对于类 cls,在序列 type.__call__cls.__new__cls.__init__ 中的每次调用中,至少会创建一个中间对象。

本 PEP 建议一个供扩展模块使用的接口。这种接口无法在没有消费者参与的情况下有效地进行测试或设计。为此,我们提供了私有(以下划线开头的)名称。API 可能会改变(基于消费者反馈)在 Python 3.9 中,我们预计它将完成,并且下划线将被移除。

规范

函数指针类型

调用通过一个函数指针进行,该指针采用以下参数

  • PyObject *callable: 被调用对象
  • PyObject *const *args: 一个参数向量
  • size_t nargs: 参数数量加上可选的标志 PY_VECTORCALL_ARGUMENTS_OFFSET(见下文)
  • PyObject *kwnames: 可能是 NULL,也可能是一个包含关键字参数名称的元组

这是由函数指针类型实现的:typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args, size_t nargs, PyObject *kwnames);

PyTypeObject 结构体的更改

未使用的插槽 printfunc tp_print 被替换为 tp_vectorcall_offset。它的类型是 Py_ssize_t。添加了一个新的 tp_flags 标志,_Py_TPFLAGS_HAVE_VECTORCALL,任何使用 vectorcall 协议的类都必须设置该标志。

如果设置了 _Py_TPFLAGS_HAVE_VECTORCALL,则 tp_vectorcall_offset 必须是正整数。它是对象中类型为 vectorcallfunc 的 vectorcall 函数指针的偏移量。该指针可能是 NULL,在这种情况下,行为与未设置 _Py_TPFLAGS_HAVE_VECTORCALL 相同。

The tp_print 插槽被重用为 tp_vectorcall_offset 插槽,以便外部项目更容易将 vectorcall 协议移植到早期 Python 版本。特别是,Cython 项目已表示对此感兴趣(请参阅 https://mail.python.org/pipermail/python-dev/2018-June/153927.html)。

描述符行为

指定了一个额外的类型标志:Py_TPFLAGS_METHOD_DESCRIPTOR

Py_TPFLAGS_METHOD_DESCRIPTOR 应该在可调用对象使用描述符协议创建绑定方法类对象时设置。解释器使用此方法来避免在调用方法时创建临时对象(请参阅 _PyObject_GetMethodLOAD_METHOD/CALL_METHOD 操作码)。

具体来说,如果 Py_TPFLAGS_METHOD_DESCRIPTOR 针对 type(func) 设置,那么

  • func.__get__(obj, cls)(*args, **kwds)(其中 obj 不为 None)必须等效于 func(obj, *args, **kwds)
  • func.__get__(None, cls)(*args, **kwds) 必须等效于 func(*args, **kwds)

对象 func.__get__(obj, cls) 没有限制。后者不需要实现 vectorcall 协议。

调用

调用采用 ((vectorcallfunc)(((char *)o)+offset))(o, args, n, kwnames) 的形式,其中 offsetPy_TYPE(o)->tp_vectorcall_offset。调用者负责创建 kwnames 元组,并确保其中没有重复项。

n 是位置参数数量加上可能存在的 PY_VECTORCALL_ARGUMENTS_OFFSET 标志。

PY_VECTORCALL_ARGUMENTS_OFFSET

如果被调用者允许暂时更改 args[-1],则应将标志 PY_VECTORCALL_ARGUMENTS_OFFSET 添加到 n 中。换句话说,如果 args 指向已分配向量中的参数 1,则可以使用此方法。被调用者必须在返回之前恢复 args[-1] 的值。

无论何时它们能够廉价地做到(无需分配),调用者都应鼓励使用 PY_VECTORCALL_ARGUMENTS_OFFSET。这样做将使绑定方法等可调用对象能够廉价地进行后续调用。字节码解释器已经在堆栈上分配了可调用对象的内存空间,因此它可以免费使用此技巧。

请参阅 [3],了解被调用者如何使用 PY_VECTORCALL_ARGUMENTS_OFFSET 来避免分配的示例。

为了从参数 n 中获取实际的参数数量,必须使用宏 PyVectorcall_NARGS(n)。这允许进行未来的更改或扩展。

新的 C API 和对 CPython 的更改

以下函数或宏已添加到 C API 中

  • PyObject *_PyObject_Vectorcall(PyObject *obj, PyObject *const *args, size_t nargs, PyObject *keywords): 使用给定参数调用 obj。请注意,nargs 可能包含标志 PY_VECTORCALL_ARGUMENTS_OFFSET。位置参数的实际数量由 PyVectorcall_NARGS(nargs) 给出。参数 keywords 是一个关键字名称元组或 NULL。空元组的效果与传递 NULL 相同。这在内部使用向量调用协议或 tp_call;如果两者都不支持,则会引发异常。
  • PyObject *PyVectorcall_Call(PyObject *obj, PyObject *tuple, PyObject *dict): 使用旧的 *args**kwargs 调用约定调用对象(必须支持向量调用)。这主要用于放入 tp_call 槽位。
  • Py_ssize_t PyVectorcall_NARGS(size_t nargs): 给定向量调用 nargs 参数,返回实际参数数量。当前等效于 nargs & ~PY_VECTORCALL_ARGUMENTS_OFFSET

子类化

扩展类型从基类继承类型标志 _Py_TPFLAGS_HAVE_VECTORCALL 和值 tp_vectorcall_offset,前提是它们以与基类相同的方式实现 tp_call。此外,如果 tp_descr_get 以与基类相同的方式实现,则继承标志 Py_TPFLAGS_METHOD_DESCRIPTOR

堆类型永远不会继承向量调用协议,因为这将不安全(堆类型可以动态更改)。此限制将来可能会解除,但这将需要在 type.__setattribute__ 中对 __call__ 进行特殊处理。

完成 API

名称 _PyObject_Vectorcall_Py_TPFLAGS_HAVE_VECTORCALL 中的下划线表示此 API 可能会在次要 Python 版本中更改。当最终确定(计划在 Python 3.9 中完成)时,它们将被重命名为 PyObject_VectorcallPy_TPFLAGS_HAVE_VECTORCALL。旧的下划线前缀名称将作为别名保留。

新 API 将像往常一样进行记录,但会警告上述内容。

本 PEP 中引入的其他名称的语义 (PyVectorcall_NARGSPyVectorcall_CallPy_TPFLAGS_METHOD_DESCRIPTORPY_VECTORCALL_ARGUMENTS_OFFSET) 是最终的。

内部 CPython 更改

对现有类的更改

functionbuiltin_function_or_methodmethod_descriptormethodwrapper_descriptormethod-wrapper 将使用向量调用协议(并非所有这些类将在初始实现中更改)。

对于 builtin_function_or_methodmethod_descriptor(使用 PyMethodDef 数据结构),可以为每个现有的调用约定实现一个特定的向量调用包装器。是否值得这样做还有待观察。

将 vectorcall 协议用于类

对于类 cls,使用 cls(xxx) 创建新实例需要多次调用。对于序列 type.__call__cls.__new__cls.__init__ 中的每个调用,至少会创建一个中间对象。因此,对于调用类使用向量调用非常有意义。这实际上意味着为 type 实现向量调用协议。一些最常用的类将使用此协议,可能包括 rangeliststrtype

The PyMethodDef 协议和 Argument Clinic

Argument Clinic [4] 自动生成围绕低级可调用对象的包装函数,提供对原始类型的安全拆箱和其他安全检查。Argument Clinic 可以扩展为生成符合新的 vectorcall 协议的包装对象。这将允许执行仅通过一次间接调用从调用者流向 Argument Clinic 生成的包装器,然后流向手工编写的代码。

使用 vectorcall 的第三方扩展类

为了实现与 Python 函数和内置函数相当的调用性能,第三方可调用对象应包含一个 vectorcallfunc 函数指针,将 tp_vectorcall_offset 设置为正确的值,并添加 _Py_TPFLAGS_HAVE_VECTORCALL 标志。执行此操作的任何类都必须实现 tp_call 函数,并确保其行为与 vectorcallfunc 函数一致。将 tp_call 设置为 PyVectorcall_Call 就足够了。

这些更改的性能影响

本 PEP 应该不会对现有代码的性能产生太大影响(无论是正面还是负面)。它主要旨在允许编写高效的新代码,而不是使现有代码更快。

尽管如此,本 PEP 针对 METH_FASTCALL 函数进行了优化。使用 METH_VARARGS 的函数的性能将略有下降。

稳定 ABI

本 PEP 中的内容不会添加到稳定 ABI (PEP 384) 中。

替代建议

bpo-29259

PEP 590 接近 bpo-29259 [1] 中提出的内容。主要区别在于本 PEP 将函数指针存储在实例中,而不是在类中。这对在 C 中实现函数更有意义,因为每个实例对应于不同的 C 函数。它还允许优化 type.__call__,而 bpo-29259 则无法做到这一点。

PEP 576 和 PEP 580

PEP 576PEP 580 的目的是使第三方对象既能表达又能高效(与 CPython 对象相当)。本 PEP 的目的是为 CPython 生态系统中的对象调用提供一种统一的方式,这种方式既能表达,又能尽可能高效。

本 PEP 的范围比 PEP 576 更广,它使用变量而不是固定偏移函数指针。底层的调用约定是类似的。因为 PEP 576 仅允许对函数指针进行固定偏移,所以它不允许对任何布局受限的对象进行改进。

PEP 580 提出对用于定义内置函数的 PyMethodDef 协议进行重大更改。本 PEP 以新的调用约定的形式提供了一种更通用、更简单的机制。本 PEP 还扩展了 PyMethodDef 协议,但仅仅是为了正式化现有的约定。

其他被拒绝的方法

考虑了一种更长的、6 个参数的形式,它将向量和可选元组和字典参数组合在一起。但是,发现将它转换为旧的 tp_call 形式的代码过于笨拙且效率低下。此外,由于 x64 Windows 上的寄存器中只传递了 4 个参数,因此这两个额外的参数会带来不可忽略的成本。

还考虑过删除任何特殊情况,并使所有调用使用 tp_call 形式。但是,除非找到一种更有效的方法来创建和销毁元组,以及在较小程度上创建和销毁字典,否则它将过于缓慢。

致谢

Victor Stinner 开发了 CPython 内部使用的原始“fastcall”调用约定。本 PEP 对其工作进行了规范和扩展。

参考文献

参考实现

可以在 https://github.com/markshannon/cpython/tree/vectorcall-minimal 找到一个最小实现。


来源:https://github.com/python/peps/blob/main/peps/pep-0590.rst

最后修改时间:2024-06-01 20:09:32 GMT