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
),在这些模块中将模块变量以及内置函数绑定为常量也是有意义的。
问题和解答
- 这会让每个人都把注意力转移到优化问题上吗?
由于它是自动完成的,因此减少了对优化的思考需求。
- 简而言之,它是如何工作的?
每个函数都有其字节码(Python 虚拟机的语言)和常量表相关的属性。bind 函数扫描字节码以查找
LOAD_GLOBAL
指令,并检查该值是否已知。如果是,它会将该值添加到常量表中,并将操作码替换为LOAD_CONSTANT
。 - 它何时有效?
当模块第一次导入时,python 会编译字节码并运行绑定优化。后续导入会重复使用之前的工作。每次会话都会重复此过程(结果不会保存在
pyc
文件中)。 - 如何知道它有效?
我实现了它,将其应用于库中的每个模块,测试套件均未出现异常。
- 如果模块定义了遮蔽内置函数的变量会怎样?
确实会发生这种情况。例如,True 可以像这样在模块级别重新定义:
True = (1==1)
。下面的示例实现会检测到遮蔽,并保留全局查找不变。 - 你是第一个认识到大多数全局查找都是针对从不更改的值的人吗?
不,这一点早已为人所知。Skip Montanaro 在 PEP 266 中对此进行了详尽的解释。
- 如果我想替换内置函数模块并提供自己的实现会怎样?
在导入模块之前执行此操作,或者只是重新加载模块,或者禁用
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!
请注意,自动检测到没有字节码 [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