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 引入了一个新的 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_GetMethod
和 LOAD_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)
的形式,其中 offset
是 Py_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_Vectorcall
和 Py_TPFLAGS_HAVE_VECTORCALL
。旧的下划线前缀名称将作为别名保留。
新 API 将像往常一样进行记录,但会警告上述内容。
本 PEP 中引入的其他名称的语义 (PyVectorcall_NARGS
、PyVectorcall_Call
、Py_TPFLAGS_METHOD_DESCRIPTOR
、PY_VECTORCALL_ARGUMENTS_OFFSET
) 是最终的。
内部 CPython 更改
对现有类的更改
类 function
、builtin_function_or_method
、method_descriptor
、method
、wrapper_descriptor
、method-wrapper
将使用向量调用协议(并非所有这些类将在初始实现中更改)。
对于 builtin_function_or_method
和 method_descriptor
(使用 PyMethodDef
数据结构),可以为每个现有的调用约定实现一个特定的向量调用包装器。是否值得这样做还有待观察。
将 vectorcall 协议用于类
对于类 cls
,使用 cls(xxx)
创建新实例需要多次调用。对于序列 type.__call__
、cls.__new__
、cls.__init__
中的每个调用,至少会创建一个中间对象。因此,对于调用类使用向量调用非常有意义。这实际上意味着为 type
实现向量调用协议。一些最常用的类将使用此协议,可能包括 range
、list
、str
和 type
。
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 576 和 PEP 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