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 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__初始化,__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_cellsNULL。在這種情況下,程式碼物件的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.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。但根據提案,在 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_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會有一個針對該鍵的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

最後修改:2025-02-01 08:55:40 GMT