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 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 的专业化代码 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 函数时没有开销。

实施

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

最后修改:2025-02-01 08:59:27 GMT