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

Python 增强提案

PEP 329 - 将内置函数视为标准库中的常量

作者:
Raymond Hettinger <python at rcn.com>
状态:
已拒绝
类型:
标准跟踪
创建时间:
2004-04-18
Python 版本:
2.4
历史记录:
2004-04-18

目录

摘要

该提案是添加一个函数来将内置引用视为常量,并在整个标准库中应用该函数。

状态

该 PEP 被作者自行拒绝。尽管 ASPN 食谱得到了很好的接受,但人们不太愿意考虑将它纳入核心发行版。

Jython 实现不使用字节码,因此如果删除了当前的 _len=len 优化,其性能将受到影响。

此外,修改字节码是最不干净的性能提升方式之一,而且会使代码更难编写。更强大的解决方案可能涉及编译器pragma 指令或指示哪些可以优化的元变量(类似于 const/volatile 声明)。

动机

库中包含诸如 _len=len 的代码,其目的是创建快速本地引用,而不是速度较慢的全局查找。尽管这些结构对性能是必要的,但它们使代码混乱,而且通常是不完整的(错失了许多机会)。

如果该提案被采纳,这些结构将可以从代码库中删除,同时在性能方面也比以前更好。

库中目前有超过 100 个 while 1 的实例。它们没有被更易读的 while True 替换,因为性能原因(编译器无法消除测试,因为 True 不一定是一个常量)。将 True 转换为常量将使代码更清晰,同时保持性能。

许多其他基本的 Python 操作由于全局查找而运行速度慢得多。在 try/except 语句中,捕获的异常会在测试它们是否匹配之前进行动态查找。类似地,简单的身份测试(例如 while x is not None)要求在每次循环中重新查找 None 变量。内置查找尤其糟糕,因为必须首先检查封闭的全局范围。这些查找链吞噬了原本应用于其他地方的缓存空间。

简而言之,如果该提案被采纳,代码将变得更干净,性能将在各个方面得到提升。

提案

添加一个名为 codetweaks.py 的模块,其中包含两个函数,bind_constants()bind_all()。第一个函数执行常量绑定,第二个递归地将其应用于目标模块中的每个函数和类。

对于标准库中的大多数模块,请在脚本末尾附近添加一对行

import codetweaks, sys
codetweaks.bind_all(sys.modules[__name__])

除了绑定内置函数外,还有一些模块(如 sre_compile),在这些模块中将模块变量以及内置函数绑定为常量也是有意义的。

问题和解答

  1. 这会让每个人都把注意力转移到优化问题上吗?

    由于它是自动完成的,因此减少了对优化的思考需求。

  2. 简而言之,它是如何工作的?

    每个函数都有其字节码(Python 虚拟机的语言)和常量表相关的属性。bind 函数扫描字节码以查找 LOAD_GLOBAL 指令,并检查该值是否已知。如果是,它会将该值添加到常量表中,并将操作码替换为 LOAD_CONSTANT

  3. 它何时有效?

    当模块第一次导入时,python 会编译字节码并运行绑定优化。后续导入会重复使用之前的工作。每次会话都会重复此过程(结果不会保存在 pyc 文件中)。

  4. 如何知道它有效?

    我实现了它,将其应用于库中的每个模块,测试套件均未出现异常。

  5. 如果模块定义了遮蔽内置函数的变量会怎样?

    确实会发生这种情况。例如,True 可以像这样在模块级别重新定义: True = (1==1)。下面的示例实现会检测到遮蔽,并保留全局查找不变。

  6. 你是第一个认识到大多数全局查找都是针对从不更改的值的人吗?

    不,这一点早已为人所知。Skip Montanaro 在 PEP 266 中对此进行了详尽的解释。

  7. 如果我想替换内置函数模块并提供自己的实现会怎样?

    在导入模块之前执行此操作,或者只是重新加载模块,或者禁用 codetweaks.py(它将有一个禁用标志)。

  8. 该模块对 Python 字节码更改的敏感程度如何?

    它会导入 opcode.py 以防止重新编号。此外,它使用的是 LOAD_CONSTLOAD_GLOBAL,它们是基础操作,一直存在。尽管如此,编码方案可能会发生变化,并且此实现将不得不与 dis 等模块一起发生变化,这些模块也依赖于当前的编码方案。

  9. 启动时间会受到什么影响?

    我没有测出任何差异。除了 warnings.py 之外,没有一个启动模块被绑定。此外,绑定函数速度非常快,只需对代码字符串进行一次扫描以查找 LOAD_GLOBAL 操作码。

示例实现

下面是 codetweaks.py 的示例实现

from types import ClassType, FunctionType
from opcode import opmap, HAVE_ARGUMENT, EXTENDED_ARG
LOAD_GLOBAL, LOAD_CONST = opmap['LOAD_GLOBAL'], opmap['LOAD_CONST']
ABORT_CODES = (EXTENDED_ARG, opmap['STORE_GLOBAL'])

def bind_constants(f, builtin_only=False, stoplist=[], verbose=False):
    """ Return a new function with optimized global references.

    Replaces global references with their currently defined values.
    If not defined, the dynamic (runtime) global lookup is left undisturbed.
    If builtin_only is True, then only builtins are optimized.
    Variable names in the stoplist are also left undisturbed.
    If verbose is True, prints each substitution as is occurs.

    """
    import __builtin__
    env = vars(__builtin__).copy()
    stoplist = dict.fromkeys(stoplist)
    if builtin_only:
        stoplist.update(f.func_globals)
    else:
        env.update(f.func_globals)

    co = f.func_code
    newcode = map(ord, co.co_code)
    newconsts = list(co.co_consts)
    codelen = len(newcode)

    i = 0
    while i < codelen:
        opcode = newcode[i]
        if opcode in ABORT_CODES:
            return f    # for simplicity, only optimize common cases
        if opcode == LOAD_GLOBAL:
            oparg = newcode[i+1] + (newcode[i+2] << 8)
            name = co.co_names[oparg]
            if name in env and name not in stoplist:
                value = env[name]
                try:
                    pos = newconsts.index(value)
                except ValueError:
                    pos = len(newconsts)
                    newconsts.append(value)
                newcode[i] = LOAD_CONST
                newcode[i+1] = pos & 0xFF
                newcode[i+2] = pos >> 8
                if verbose:
                    print name, '-->', value
        i += 1
        if opcode >= HAVE_ARGUMENT:
            i += 2

    codestr = ''.join(map(chr, newcode))
    codeobj = type(co)(co.co_argcount, co.co_nlocals, co.co_stacksize,
                    co.co_flags, codestr, tuple(newconsts), co.co_names,
                    co.co_varnames, co.co_filename, co.co_name,
                    co.co_firstlineno, co.co_lnotab, co.co_freevars,
                    co.co_cellvars)
    return type(f)(codeobj, f.func_globals, f.func_name, f.func_defaults,
                    f.func_closure)


def bind_all(mc, builtin_only=False, stoplist=[], verbose=False):
    """Recursively apply bind_constants() to functions in a module or class.

    Use as the last line of the module (after everything is defined, but
    before test code).

    In modules that need modifiable globals, set builtin_only to True.

    """
    for k, v in vars(mc).items():
        if type(v) is FunctionType:
            newv = bind_constants(v, builtin_only, stoplist, verbose)
            setattr(mc, k, newv)
        elif type(v) in (type, ClassType):
            bind_all(v, builtin_only, stoplist, verbose)


def f(): pass
try:
    f.func_code.code
except AttributeError:                  # detect non-CPython environments
    bind_all = lambda *args, **kwds: 0
del f

import sys
bind_all(sys.modules[__name__])         # Optimizer, optimize thyself!

请注意,自动检测到没有字节码 [2] 的非 CPython 环境。在这种情况下,bind 函数将简单地返回原始函数,保持不变。这确保了对库模块的两次添加不会影响其他实现。

最终的代码应该添加一个标志,使其更容易禁用绑定。

参考文献

[1] 非私有实现的 ASPN 食谱 https://code.activestate.com/recipes/277940/


来源:https://github.com/python/peps/blob/main/peps/pep-0329.rst

最后修改时间:2023-09-09 17:39:29 GMT