PEP 329 – 将内置函数视为常量在标准库中使用
- 作者:
- Raymond Hettinger <python at rcn.com>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2004 年 4 月 18 日
- Python 版本:
- 2.4
- 发布历史:
- 2004 年 4 月 18 日
摘要
该提案旨在添加一个函数,用于将内置函数引用视为常量,并在整个标准库中应用该函数。
状态
该 PEP 已被作者自行拒绝。尽管 ASPN 上的食谱受到了好评,但人们对将其纳入核心发行版的意愿较低。
Jython 实现不使用字节码,因此如果移除当前的 _len=len
优化,其性能会受到影响。
此外,改变字节码是提高性能和实现更清晰代码的最不干净的方法之一。更健壮的解决方案可能需要编译器 pragma 指令或指示哪些内容可以被优化的元变量(类似于 const/volatile 声明)。
动机
该库包含诸如 _len=len
之类的代码,旨在创建快速的局部引用,而不是较慢的全局查找。虽然这是性能所必需的,但这些构造会使代码混乱,并且通常不完整(错失了许多机会)。
如果该提案被采纳,这些构造就可以从代码库中消除,同时在性能方面也能改进其结果。
库中目前有超过一百处 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
)也适合将模块变量和内置函数绑定为常量。
问答
- 这会迫使每个人都关注优化问题吗?
因为它是在自动完成的,所以减少了思考优化的需求。
- 简而言之,它是如何工作的?
每个函数都有其字节码(Python 虚拟机语言)的属性和一个常量表。bind 函数扫描字节码中的
LOAD_GLOBAL
指令,并检查值是否已知。如果是,它会将该值添加到常量表中,并将操作码替换为LOAD_CONSTANT
。 - 它何时起作用?
当一个模块首次导入时,Python 会编译字节码并运行绑定优化。后续的导入将重用之前的工作。每个会话都会重复此过程(结果不会保存在
pyc
文件中)。 - 你怎么知道它有效?
我实现了它,将其应用于库中的每个模块,并且测试套件无异常运行。
- 如果模块定义了一个变量来隐藏(shadowing)一个内置函数怎么办?
这确实会发生。例如,True 可以在模块级别重新定义为
True = (1==1)
。下面的示例实现会检测到隐藏,并保持全局查找不变。 - 你是否第一个认识到大多数全局查找都是针对永不改变的值?
不,这一点早已为人所知。Skip Montanaro 在 PEP 266 中给出了一个清晰的解释。
- 如果我想替换 builtins 模块并提供我自己的实现怎么办?
要么在导入模块之前执行此操作,要么重新加载模块,或者禁用
codetweaks.py
(它会有一个禁用标志)。 - 该模块对 Python 字节码的变化有多敏感?
它导入
opcode.py
以防止重新编号。此外,它使用LOAD_CONST
和LOAD_GLOBAL
,这些是基础且一直存在的。尽管如此,编码方案可能会改变,并且此实现以及像dis
这样依赖于当前编码方案的模块也必须随之改变。 - 对启动时间有什么影响?
我无法测量到差异。除 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!
注意自动检测非 CPython 环境(该环境没有字节码)[2]。在这种情况下,bind 函数只会返回原始函数不变。这确保了添加到库模块的两行代码不会影响其他实现。
最终代码应添加一个标志,以便于禁用绑定。
参考资料
[1] ASPN 的非私有实现食谱 https://code.activestate.com/recipes/277940/
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0329.rst