PEP 280 – 优化全局变量访问
- 作者:
- Guido van Rossum <guido at python.org>
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 创建日期:
- 2002年2月10日
- Python 版本:
- 2.3
- 发布历史:
推迟
摘要
本PEP描述了另一种优化模块全局变量访问的方法,为PEP 266(由Skip Montanaro提出的优化全局变量/属性访问)和PEP 267(由Jeremy Hylton提出的优化模块命名空间访问)提供了替代方案。
预期最终将选择并实现一种方法;也可能先对多种方法进行原型设计。
描述
(注:Jason Orendorff 写道:“”“我很久以前,大概在Python 1.5时代,曾实现过这个。我做到了只比普通Python慢15%的程度,然后就放弃了。;) 在我的实现中,‘单元格’是真正的第一类对象,而‘celldict’是字典的复制和修改版本。我忘了其余部分是如何工作的。””” 参考文献:https://mail.python.org/pipermail/python-dev/2002-February/019876.html)
让一个单元格成为一个非常简单的Python对象,包含一个指向Python对象的指针和一个指向单元格的指针。这两个指针都可以是NULL。Python实现可以是
class cell(object):
def __init__(self):
self.objptr = NULL
self.cellptr = NULL
cellptr属性用于将单元格链接在一起以搜索内置函数;这将在后面解释。
让一个celldict成为从字符串(模块全局变量的名称)到对象(这些全局变量的值)的映射,使用单元格字典实现。Python实现可以是
class celldict(object):
def __init__(self):
self.__dict = {} # dict of cells
def getcell(self, key):
c = self.__dict.get(key)
if c is None:
c = cell()
self.__dict[key] = c
return c
def cellkeys(self):
return self.__dict.keys()
def __getitem__(self, key):
c = self.__dict.get(key)
if c is None:
raise KeyError, key
value = c.objptr
if value is NULL:
raise KeyError, key
else:
return value
def __setitem__(self, key, value):
c = self.__dict.get(key)
if c is None:
c = cell()
self.__dict[key] = c
c.objptr = value
def __delitem__(self, key):
c = self.__dict.get(key)
if c is None or c.objptr is NULL:
raise KeyError, key
c.objptr = NULL
def keys(self):
return [k for k, c in self.__dict.iteritems()
if c.objptr is not NULL]
def items(self):
return [k, c.objptr for k, c in self.__dict.iteritems()
if c.objptr is not NULL]
def values(self):
preturn [c.objptr for c in self.__dict.itervalues()
if c.objptr is not NULL]
def clear(self):
for c in self.__dict.values():
c.objptr = NULL
# Etc.
可能存在与给定键对应的单元格,但该单元格的objptr为NULL;我们称之为“空单元格”。当celldict用作映射时,就好像空单元格不存在一样。但是,一旦添加,单元格就永远不会从celldict中删除,并且可以使用getcell()方法获取空单元格。
celldict实现从不使用单元格的cellptr属性。
我们将模块实现更改为使用celldict作为其__dict__。模块的getattr、setattr和delattr操作现在映射到celldict上的getitem、setitem和delitem。 <module>.__dict__和globals()的类型可能是唯一的向后不兼容性。
当模块初始化时,其__builtins__从__builtin__模块的__dict__初始化,__dict__本身是一个celldict。对于__builtins__中的每个单元格,新模块的__dict__添加一个objptr为NULL的单元格,其cellptr指向__builtins__中对应的单元格。Python伪代码(忽略rexec)
import __builtin__
class module(object):
def __init__(self):
self.__dict__ = d = celldict()
d['__builtins__'] = bd = __builtin__.__dict__
for k in bd.cellkeys():
c = self.__dict__.getcell(k)
c.cellptr = bd.getcell(k)
def __getattr__(self, k):
try:
return self.__dict__[k]
except KeyError:
raise IndexError, k
def __setattr__(self, k, v):
self.__dict__[k] = v
def __delattr__(self, k):
del self.__dict__[k]
编译器为全局变量的引用生成LOAD_GLOBAL_CELL <i>(以及STORE_GLOBAL_CELL <i>等)操作码,其中<i>是一个仅在一个代码对象中具有意义的小索引,类似于LOAD_CONST中的常量索引。代码对象有一个新的元组co_globals,它给出由代码按<i>索引引用的全局变量的名称。不需要新的分析就能做到这一点。
当从代码对象和celldict创建函数对象时,函数对象通过向celldict请求与代码对象的co_globals中名称对应的单元格来创建一个单元格指针数组。如果celldict中还没有特定名称的单元格,它会创建一个空单元格。这个单元格指针数组存储在函数对象上,作为func_cells。当从常规字典而不是celldict创建函数对象时,func_cells是一个NULL指针。
当虚拟机执行LOAD_GLOBAL_CELL <i>指令时,它从func_cells获取单元格编号<i>。然后它查看单元格的PyObject指针,如果不是NULL,那就是全局值。如果是NULL,它会沿着单元格的单元格指针到下一个单元格,如果不是NULL,则查看该单元格中的PyObject指针。如果那也是NULL,或者没有第二个单元格,则会引发NameError。(它可以沿着单元格指针链直到找到NULL单元格指针;但我没有用处。)对于STORE_GLOBAL_CELL <i>类似,除了它不遵循单元格指针链——它总是存储在第一个单元格中。
虚拟机中存在回退机制,用于处理函数全局变量不是celldict的情况,因此func_cells为NULL。在这种情况下,代码对象的co_globals会根据<i>进行索引,以找到对应全局变量的名称,然后使用此名称来索引函数的全局变量字典。
额外想法
- 永远不要让
func_cell成为NULL指针;相反,创建一个空单元格数组,这样LOAD_GLOBAL_CELL可以在不进行NULL检查的情况下索引func_cells。 - 在创建单元格时,使
c.cellptr等于c,这样LOAD_GLOBAL_CELL就可以在不进行NULL检查的情况下始终解引用c.cellptr。加入了这两个额外想法后,以下是
LOAD_GLOBAL_CELL的Python伪代码def LOAD_GLOBAL_CELL(self, i): # self is the frame c = self.func_cells[i] obj = c.objptr if obj is not NULL: return obj # Existing global return c.cellptr.objptr # Built-in or NULL
- 更激进一些:将内置函数的实际值放入模块字典中,而不仅仅是指向包含实际值的单元格的指针。
这有两点:(1) 简化并加速访问,这是最常见的操作。(2) 支持对现有极端特殊情况的忠实模拟。
关于#2,上述方案中的内置函数集在模块字典首次创建时被捕获。此后对内置函数名称集的修改不会反映在模块字典中。示例:考虑文件
main.py和cheater.py[main.py] import cheater def f(): cheater.cheat() return pachinko() print f() [cheater.py] def cheat(): import __builtin__ __builtin__.pachinko = lambda: 666
如果在 Python 2.2(或更早版本)下运行
main.py,则会打印 666。但根据提案,在 main 的__dict__初始化时,__builtin__.pachinko不存在。当创建 f 的函数对象时,main.__dict__会增长一个映射到两个NULLs的 pachinko 单元格。当调用cheat()时,__builtin__.__dict__也会增长一个 pachinko 单元格,但main.__dict__不知道——也永远不会知道——这一点。当 f 的返回语句引用 pachinko 时,它仍然会在main.__dict__的pachinko单元格中找到双 NULLs,因此会引发NameError。如果删除了模块全局变量foo,但在删除之前(但在模块字典首次创建之后)创建了一个内置foo,也可能发生类似的(原因相同的)兼容性问题。在这种情况下,在2.2及以前版本中,内置foo在模块中可见,但在本提案下仍不可见。
修改内置函数极其罕见(大多数程序从不修改内置函数,也很难想象频繁修改内置函数的合理用途——我从未见过或听说过),因此修改内置函数的开销有多大并不重要。另一方面,引用全局变量和内置函数非常常见。结合这些观察结果,建议更积极地在模块全局变量中缓存内置函数,以加速访问,但代价是使内置函数的修改(可能代价高昂得多)以保持缓存同步。
上述方案大部分保持不变,其余部分略有不同。单元格变为
class cell(object): def __init__(self, obj=NULL, builtin=0): self.objptr = obj self.builtinflag = builtin
并且celldict将字符串映射到此版本的单元格。
builtinflag当且仅当objptr包含从内置函数获得的值时为真;换句话说,当且仅当单元格作为缓存值时为真。当builtinflag为假时,objptr是模块全局变量的值(可能为NULL)。celldict变为class celldict(object): def __init__(self, builtindict=()): self.basedict = builtindict self.__dict = d = {} for k, v in builtindict.items(): d[k] = cell(v, 1) def __getitem__(self, key): c = self.__dict.get(key) if c is None or c.objptr is NULL or c.builtinflag: raise KeyError, key return c.objptr def __setitem__(self, key, value): c = self.__dict.get(key) if c is None: c = cell() self.__dict[key] = c c.objptr = value c.builtinflag = 0 def __delitem__(self, key): c = self.__dict.get(key) if c is None or c.objptr is NULL or c.builtinflag: raise KeyError, key c.objptr = NULL # We may have unmasked a builtin. Note that because # we're checking the builtin dict for that *now*, this # still works if the builtin first came into existence # after we were constructed. Note too that del on # namespace dicts is rare, so the expense of this check # shouldn't matter. if key in self.basedict: c.objptr = self.basedict[key] assert c.objptr is not NULL # else "in" lied c.builtinflag = 1 else: # There is no builtin with the same name. assert not c.builtinflag def keys(self): return [k for k, c in self.__dict.iteritems() if c.objptr is not NULL and not c.builtinflag] def items(self): return [k, c.objptr for k, c in self.__dict.iteritems() if c.objptr is not NULL and not c.builtinflag] def values(self): preturn [c.objptr for c in self.__dict.itervalues() if c.objptr is not NULL and not c.builtinflag] def clear(self): for c in self.__dict.values(): if not c.builtinflag: c.objptr = NULL # Etc.
速度优势来自于简化
LOAD_GLOBAL_CELL,我预计它的执行频率高于所有其他命名空间操作的总和def LOAD_GLOBAL_CELL(self, i): # self is the frame c = self.func_cells[i] return c.objptr # may be NULL (also true before)
也就是说,访问内置函数和访问模块全局变量的速度一样快。对于模块全局变量,可以节省一个NULL指针测试+分支。对于内置函数,还可以节省一次额外的指针追踪。
要使此方案奏效,还需要另一个代价高昂的部分,即将内置函数的修改传播到从内置函数初始化的模块字典中。这很像在2.2中,将新式基类的更改传播到它们的子类:内置函数需要维护一个指向从内置函数字典初始化的模块(或模块字典)的弱引用列表。给定对内置函数字典的修改(添加新键、更改现有键关联的值或删除键),遍历模块字典列表并对其进行相应的修改。这很简单;例如,如果从内置函数中删除了一个键,则在每个模块中执行
reflect_bltin_deldef reflect_bltin_del(self, key): c = self.__dict.get(key) assert c is not None # else we were already out of synch if c.builtinflag: # Put us back in synch. c.objptr = NULL c.builtinflag = 0 # Else we're shadowing the builtin, so don't care that # the builtin went away.
请注意,
c.builtinflag可以防止我们错误地删除同名模块全局变量。添加新的(键,值)内置对也是类似的def reflect_bltin_new(self, key, value): c = self.__dict.get(key) if c is None: # Never heard of it before: cache the builtin value. self.__dict[key] = cell(value, 1) elif c.objptr is NULL: # This used to exist in the module or the builtins, # but doesn't anymore; rehabilitate it. assert not c.builtinflag c.objptr = value c.builtinflag = 1 else: # We're shadowing it already. assert not c.builtinflag
更改现有内置函数的值
def reflect_bltin_change(self, key, newvalue): c = self.__dict.get(key) assert c is not None # else we were already out of synch if c.builtinflag: # Put us back in synch. c.objptr = newvalue # Else we're shadowing the builtin, so don't care that # the builtin changed.
常见问题
- 问:是否仍有可能
a) 在
__builtin__命名空间中安装新的内置函数,并让它们立即在所有已加载模块中可用?b) 用我自己的副本(例如,为了提高安全性)覆盖内置函数(例如
open()),从而使这些新副本覆盖所有模块中的先前副本?答:是的,这就是此设计的全部要点。在最初的方法中,当
LOAD_GLOBAL_CELL在第二个单元格中找到NULL时,它应该回溯以查看__builtins__字典是否已被修改(伪代码中尚未包含此功能)。Tim的“更激进”的替代方案也解决了这个问题。 - 问:新方案与受限执行模型如何兼容?
答:它旨在完全支持该模型。
- 问:当一个全局变量被删除时会发生什么?
答:模块的celldict会有一个针对该键的objptr为
NULL的单元格。这在两种变体中都成立,但“激进”变体会继续查看这是否揭示了同名的内置函数,如果是,则将其值(只是最终PyObject*的指针复制)复制到单元格的objptr中,并将单元格的builtinflag设置为true。 - 问:
LOAD_GLOBAL_CELL的C代码会是什么样子?答:第一个版本,结合了“额外想法”下的前两个要点,可能看起来像这样
case LOAD_GLOBAL_CELL: cell = func_cells[oparg]; x = cell->objptr; if (x == NULL) { x = cell->cellptr->objptr; if (x == NULL) { ... error recovery ... break; } } Py_INCREF(x); PUSH(x); continue;
我们甚至可以这样写(此想法由 Ka-Ping Yee 提出)
case LOAD_GLOBAL_CELL: cell = func_cells[oparg]; x = cell->cellptr->objptr; if (x != NULL) { Py_INCREF(x); PUSH(x); continue; } ... error recovery ... break;
在现代CPU架构中,这减少了内置函数分支的数量,这可能是一件非常好的事情,而任何不错的内存缓存都应该意识到
cell->cellptr与常规全局变量的cell相同,因此在这种情况下这也应该非常快。对于激进的变体
case LOAD_GLOBAL_CELL: cell = func_cells[oparg]; x = cell->objptr; if (x != NULL) { Py_INCREF(x); PUSH(x); continue; } ... error recovery ... break;
- 问:在模块的顶层代码中,假设没有
func_cells数组,会发生什么?答:我们可以做一些代码分析并创建一个
func_cells数组,或者我们可以使用LOAD_NAME,它应该在全局字典上使用PyMapping_GetItem。
图示
Ka-Ping Yee 提供了一张“import spam”后事物状态的图画,其中spam.py包含
import eggs
i = -2
max = 3
def foo(n):
y = abs(i) + max
return eggs.ham(y + n)
图画位于http://web.lfw.org/repo/cells.gif;更大的版本位于http://lfw.org/repo/cells-big.gif;源文件位于http://lfw.org/repo/cells.ai。
比较
XXX 在这里,可以添加三种方法的比较。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0280.rst