Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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
  • 如果守卫器将始终失败,则返回 1PyFunction_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 是可调用对象或代码对象,而 guardsPyFuncGuard 对象列表。在发生错误时引发异常并返回 NULL

PyFunction_GetSpecializedCode

添加一个函数方法,用于检查守卫器以选择专门化的代码

PyObject* PyFunction_GetSpecializedCode(PyObject *func,
                                        PyObject **stack,
                                        int na, int nk)

有关 stacknank 参数,请参阅守卫器的 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