PEP 510 – 使用守卫器专门化函数
- 作者:
- Victor Stinner <vstinner at python.org>
- 状态:
- 已拒绝
- 类型:
- 标准轨迹
- 创建:
- 2016年1月4日
- Python 版本:
- 3.6
拒绝通知
此 PEP 被其作者拒绝,因为该设计没有显示出任何明显的加速,而且由于缺乏时间来实现最先进和最复杂的优化。
摘要
向 Python C API 添加函数以专门化纯 Python 函数:添加带有守卫器的专门化代码。它允许实现尊重 Python 语义的静态优化器。
基本原理
Python 语义
Python 难以优化,因为几乎所有东西都是可变的:内置函数、函数代码、全局变量、局部变量……都可以在运行时修改。实现尊重 Python 语义的优化需要检测“何时发生变化”,我们将这些检查称为“守卫器”。
此 PEP 提出向 Python C API 添加一个公共 API,以便向函数添加带有守卫器的专门化代码。当调用函数时,如果没有任何变化,则使用专门化的代码,否则使用原始字节码。
即使守卫器有助于尊重 Python 语义的大部分内容,但在不对精确行为进行细微更改的情况下,也很难优化 Python。CPython 拥有悠久的历史,许多应用程序依赖于实现细节。必须在“一切都是可变的”和性能之间找到折衷方案。
编写优化器不在本 PEP 的范围内。
为什么不使用 JIT 编译器?
有多个积极开发的 Python JIT 编译器
Numba 专注于数值计算。Pyston 和 Pyjion 仍然很年轻。PyPy 是最完整的 Python 解释器,它通常在微基准测试和许多宏基准测试中比 CPython 更快,并且与 CPython 具有非常好的兼容性(它尊重 Python 语义)。Python JIT 编译器仍然存在一些问题,导致它们无法广泛用于替代 CPython。
许多流行的库,如 numpy、PyGTK、PyQt、PySide 和 wxPython,都是用 C 或 C++ 实现的,并使用 Python C API。为了拥有较小的内存占用和更好的性能,Python JIT 编译器不使用引用计数以使用更快的垃圾收集器,不使用 CPython 对象的 C 结构,并以不同的方式管理内存分配。PyPy 有一个 cpyext
模块可以模拟 Python C API,但它的性能比 CPython 差,并且不支持完整的 Python C API。
新功能首先在 CPython 中开发。在 2016 年 1 月,最新的 CPython 稳定版本是 3.5,而 PyPy 仅支持 Python 2.7 和 3.2,Pyston 仅支持 Python 2.7。
即使 PyPy 与 Python 具有非常好的兼容性,一些模块仍然与 PyPy 不兼容:请参阅 PyPy 兼容性 Wiki。Python C API 的不完整支持是此问题的一部分。PyPy 和 CPython 之间也存在细微差别,例如引用计数:对象析构函数始终在 PyPy 中被调用,但在 CPython 中可以“稍后”被调用。使用上下文管理器有助于控制何时释放资源。
即使 PyPy 在广泛的基准测试中比 CPython 快得多,一些用户仍然报告在某些特定用例中性能比 CPython 差或性能不稳定。
当 Python 用作运行时间少于 1 分钟的程序的脚本程序时,JIT 编译器可能会更慢,因为它们的启动时间更长,并且 JIT 编译器需要时间来优化代码。例如,大多数 Mercurial 命令需要几秒钟。
Numba 现在支持提前编译,但它需要装饰器来指定参数类型,并且它仅支持数值类型。
CPython 3.5 几乎没有优化:窥孔优化器仅实现基本优化。静态编译器是 CPython 3.5 和 PyPy 之间的折衷方案。
注意
还有一个 Unladen Swallow 项目,但它在 2011 年被放弃了。
示例
以下示例并非旨在展示强大的优化以实现重要的加速,而是为了简洁易懂,仅用于解释原理。
假设的 myoptimizer 模块
此 PEP 中的示例使用假设的 myoptimizer
模块,该模块提供以下函数和类型
specialize(func, code, guards)
:向函数func
添加带有守卫器guards
的专门化代码code
get_specialized(func)
:将专门化代码的列表作为(code, guards)
元组列表获取,其中code
是可调用对象或代码对象,而guards
是守卫器列表GuardBuiltins(name)
:监视builtins.__dict__[name]
和globals()[name]
的守卫器。如果替换了builtins.__dict__[name]
,或者设置了globals()[name]
,则守卫器失败。
使用字节码
添加专门化的字节码,其中对纯内置函数 chr(65)
的调用被替换为其结果 "A"
import myoptimizer
def func():
return chr(65)
def fast_func():
return "A"
myoptimizer.specialize(func, fast_func.__code__,
[myoptimizer.GuardBuiltins("chr")])
del fast_func
显示守卫器行为的示例
print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
print()
import builtins
builtins.chr = lambda obj: "mock"
print("func(): %s" % func())
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
输出
func(): A
#specialized: 1
func(): mock
#specialized: 0
第一个调用使用专门化的字节码,该字节码返回字符串 "A"
。第二个调用删除专门化的代码,因为替换了内置 chr()
函数,并执行调用 chr(65)
的原始字节码。
在微基准测试中,调用专门化的字节码需要 88 ns,而原始函数需要 145 ns(+57 ns):速度提高了 1.6 倍。
使用内置函数
添加 C 内置 chr()
函数作为专门化代码,而不是调用 chr(obj)
的字节码
import myoptimizer
def func(arg):
return chr(arg)
myoptimizer.specialize(func, chr,
[myoptimizer.GuardBuiltins("chr")])
显示守卫器行为的示例
print("func(65): %s" % func(65))
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
print()
import builtins
builtins.chr = lambda obj: "mock"
print("func(65): %s" % func(65))
print("#specialized: %s" % len(myoptimizer.get_specialized(func)))
输出
func(): A
#specialized: 1
func(): mock
#specialized: 0
第一个调用调用 C 内置 chr()
函数(无需创建 Python 框架)。第二个调用删除专门化的代码,因为替换了内置 chr()
函数,并执行原始字节码。
在微基准测试中,调用 C 内置函数需要 95 ns,而原始字节码需要 155 ns(+60 ns):速度提高了 1.6 倍。直接调用 chr(65)
需要 76 ns。
选择专门化的代码
选择专门化代码以调用纯 Python 函数的伪代码
def call_func(func, args, kwargs):
specialized = myoptimizer.get_specialized(func)
nspecialized = len(specialized)
index = 0
while index < nspecialized:
specialized_code, guards = specialized[index]
for guard in guards:
check = guard(args, kwargs)
if check:
break
if not check:
# all guards succeeded:
# use the specialized code
return specialized_code
elif check == 1:
# a guard failed temporarily:
# try the next specialized code
index += 1
else:
assert check == 2
# a guard will always fail:
# remove the specialized code
del specialized[index]
# if a guard of each specialized code failed, or if the function
# has no specialized code, use original bytecode
code = func.__code__
更改
对 Python C API 的更改
- 添加
PyFuncGuardObject
对象和PyFuncGuard_Type
类型 - 添加
PySpecializedCode
结构 - 向
PyFunctionObject
结构添加以下字段Py_ssize_t nb_specialized; PySpecializedCode *specialized;
- 添加函数方法
PyFunction_Specialize()
PyFunction_GetSpecializedCodes()
PyFunction_GetSpecializedCode()
PyFunction_RemoveSpecialized()
PyFunction_RemoveAllSpecialized()
这些函数和类型均未在 Python 层面公开。
所有这些添加都明确地不包含在稳定 ABI 中。
当替换函数代码时 (func.__code__ = new_code
),将删除所有专门化代码和守卫器。
函数守卫器
添加函数守卫器对象
typedef struct {
PyObject ob_base;
int (*init) (PyObject *guard, PyObject *func);
int (*check) (PyObject *guard, PyObject **stack, int na, int nk);
} PyFuncGuardObject;
init()
函数初始化守卫器
- 成功时返回
0
- 如果守卫器将始终失败,则返回
1
:PyFunction_Specialize()
必须忽略专门化的代码 - 在发生错误时引发异常并返回
-1
check()
函数检查守卫器
- 成功时返回
0
- 如果守卫器暂时失败,则返回
1
- 如果守卫器将始终失败,则返回
2
:必须删除专门化的代码 - 在发生错误时引发异常并返回
-1
stack 是参数数组:索引参数后跟关键字参数的 (key, value) 对。na 是索引参数的数量。nk 是关键字参数的数量: (key, value) 对的数量。 stack
包含 na + nk * 2
个对象。
专门化的代码
添加专门化代码结构
typedef struct {
PyObject *code; /* callable or code object */
Py_ssize_t nb_guard;
PyObject **guards; /* PyFuncGuardObject objects */
} PySpecializedCode;
函数方法
PyFunction_Specialize
添加一个函数方法来专门化函数,添加带有守卫器的专门化代码
int PyFunction_Specialize(PyObject *func,
PyObject *code, PyObject *guards)
如果 code 是 Python 函数,则 code 函数的代码对象用作专门化代码。专门化的 Python 函数必须具有相同的参数默认值、相同的关键字参数默认值,并且不得具有专门化代码。
如果 code 是 Python 函数或代码对象,则会创建一个新的代码对象,并复制 func 代码对象 的代码名称和第一行号。专门化的代码必须具有相同的单元变量和相同的自由变量。
结果
- 成功时返回
0
- 如果已忽略专门化,则返回
1
- 在发生错误时引发异常并返回
-1
PyFunction_GetSpecializedCodes
添加一个函数方法来获取专门化代码的列表
PyObject* PyFunction_GetSpecializedCodes(PyObject *func)
返回 (code, guards) 元组列表,其中 code 是可调用对象或代码对象,而 guards 是 PyFuncGuard
对象列表。在发生错误时引发异常并返回 NULL
。
PyFunction_GetSpecializedCode
添加一个函数方法,用于检查守卫器以选择专门化的代码
PyObject* PyFunction_GetSpecializedCode(PyObject *func,
PyObject **stack,
int na, int nk)
有关 stack、na 和 nk 参数,请参阅守卫器的 check()
函数。成功时返回可调用对象或代码对象。在发生错误时引发异常并返回 NULL
。
PyFunction_RemoveSpecialized
添加一个函数方法,用于通过其索引删除带有其守卫器的专门化代码
int PyFunction_RemoveSpecialized(PyObject *func, Py_ssize_t index)
成功或索引不存在时返回0
。错误时抛出异常并返回-1
。
PyFunction_RemoveAllSpecialized
添加一个函数方法,用于移除函数的所有专用代码和保护代码。
int PyFunction_RemoveAllSpecialized(PyObject *func)
成功时返回0
。如果func不是函数,则抛出异常并返回-1
。
基准测试
在python3.6 -m timeit -s 'def f(): pass' 'f()'
上进行微基准测试(最佳3次运行)。
- 原始 Python:79 ns
- 修补后的 Python:79 ns
根据此微基准测试,更改对调用没有特殊化的 Python 函数没有开销。
实现
该问题 #26098:PEP 510:使用保护代码专门化函数包含一个实现此 PEP 的补丁。
其他 Python 实现
此 PEP 仅包含对 Python C API 的更改,Python API 未更改。Python 的其他实现可以自由地不实现新的添加,或将添加的函数实现为无操作。
PyFunction_Specialize()
:始终返回1
(已忽略专门化)。PyFunction_GetSpecializedCodes()
:始终返回一个空列表。PyFunction_GetSpecializedCode()
:返回函数代码对象,如同现有的PyFunction_GET_CODE()
宏。
讨论
python-ideas 邮件列表上的线程:RFC:PEP:使用保护代码专门化函数。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0510.rst
上次修改时间:2023-09-09 17:39:29 GMT