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

Python 增强提案

PEP 280 – 优化全局变量访问

作者:
Guido van Rossum <guido at python.org>
状态:
延迟
类型:
标准跟踪
创建:
2002年2月10日
Python 版本:
2.3
历史记录:


目录

延迟

虽然此 PEP 想法不错,但还没有人着手解决此 PEP 与PEP 266PEP 267 之间的差异。因此,它被延迟了。

摘要

此 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__初始化,后者本身就是一个 celldict。对于__builtins__中的每个单元格,新模块的__dict__添加一个具有NULL objptr 的单元格,其 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指针。

当 VM 执行LOAD_GLOBAL_CELL <i>指令时,它从func_cells获取单元格编号<i>。然后它查看单元格的PyObject指针,如果它不是NULL,那就是全局值。如果是NULL,则它遵循单元格的单元格指针到下一个单元格(如果它不是NULL),并在该单元格的PyObject指针中查找。如果这也是NULL,或者没有第二个单元格,则引发NameError。(它可以沿着单元格指针链一直走到找到一个NULL单元格指针;但我对此没有用。)对于STORE_GLOBAL_CELL <i>类似,除了它不遵循单元格指针链——它始终存储在第一个单元格中。

对于函数的全局变量不是 celldict 的情况,VM 中存在回退,因此func_cellsNULL。在这种情况下,使用<i>索引代码对象的co_globals以查找相应全局变量的名称,并使用此名称来索引函数的全局变量字典。

其他想法

  • 不要使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.pycheater.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。但在该提案下,__builtin__.pachinko在初始化 main 的__dict__时不存在。创建函数对象 f 时,main.__dict__会增加一个 pachinko 单元格,映射到两个NULLs。调用cheat()时,__builtin__.__dict__也会增加一个 pachinko 单元格,但main.__dict__不知道——也永远不会知道——这一点。当 f 的 return stmt 引用 pachinko 时,它仍然会在main.__dict__pachinko单元格中找到双 NULL,因此会引发NameError

    如果删除了模块全局变量 foo,但在模块字典首次创建后但内置函数 foo 创建之前创建了内置函数 foo,则可能会发生类似(原因)的兼容性中断。然后,内置函数 foo 在 2.2 及之前版本中变得可见,但在该提案下仍然不可见。

    修改内置函数非常罕见(大多数程序从不修改内置函数,并且很难想象频繁修改内置函数的合理用途——我从未见过或听说过),因此修改内置函数变得多么昂贵并不重要。另一方面,引用全局变量和内置函数非常普遍。结合这些观察结果表明,在模块全局变量中更积极地缓存内置函数,从而加快访问速度,但代价是使修改内置函数(可能要昂贵得多)变得更昂贵以保持缓存同步。

    上述方案的大部分内容保持不变,其余大部分内容也略有不同。单元格更改为

    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)
    

    也就是说,访问内置函数和访问模块全局变量的速度一样快。对于模块全局变量,节省了一个空指针测试+分支。对于内置函数,也节省了一个额外的指针追溯。

    要使这个方案可行,还需要解决另一部分问题,即将内置函数的修改传播到从内置函数初始化的模块字典中。这很像在 Python 2.2 中将新式基类的更改传播到其子类:内置函数需要维护一个指向从内置函数字典初始化的模块(或模块字典)的弱引用列表。如果内置函数字典发生了修改(添加新键、更改现有键关联的值或删除键),则遍历模块字典列表并对其进行相应的修改。这很简单;例如,如果从内置函数中删除了一个键,则在每个模块中执行reflect_bltin_del

    def 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 将包含一个单元格,该单元格对于该键具有NULL objptr。这在两种变体中都是正确的,但“积极”的变体继续检查这是否会屏蔽同名的内置函数,如果是,则将其值(只是最终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 提供了一张“导入 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

上次修改:2023-09-09 17:39:29 GMT