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 Compatibility 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的专业化代码codeget_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 函数时没有开销。
实施
在 issue #26098: PEP 510: Specialize functions with guards 中包含了一个实现此 PEP 的补丁。
其他 Python 实现
此 PEP 仅包含对 Python C API 的更改,Python API 保持不变。其他 Python 实现可以自由地不实现新添加的功能,或将添加的函数实现为 no-op。
PyFunction_Specialize():始终返回1(专业化已被忽略)PyFunction_GetSpecializedCodes():始终返回一个空列表PyFunction_GetSpecializedCode():返回函数代码对象,就像现有的PyFunction_GET_CODE()宏一样
讨论
Python-ideas 邮件列表上的讨论:RFC: PEP: Specialized functions with guards。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0510.rst