如何將Python內存佔用縮小20倍?
當程序執行過程中RAM中有大量對象處於活動狀態時,可能會出現內存問題,特別是在對可用內存總量有限制的情況下。
下面概述了一些減小對象大小的方法,這些方法可以顯著減少純Python程序所需的RAM數量。
注: 這是我原帖子的英文版本(原帖子是用俄文寫的)。(https://habr.com/ru/post/455722/ )
為了簡單起見,我們將考慮用Python中的結構來表示坐標為x、y、z的點,並通過名稱來訪問坐標值。
Dict
在小程序中,特別是在腳本中,使用內置的dict來表示結構信息是非常簡單方便的:
隨著Python 3.6中使用一組有序鍵的更緊湊實現方式的出現,dict變得更有吸引力。但是,讓我們看看它在RAM中的內存大小:
它需要大量內存,特別是當你突然需要創建大量實例時:
類實例
對於那些喜歡將所有東西放置在類中的人來說,最好將結構定義為一個可以通過屬性名訪問的類,:
類實例的結構很有趣:
這裡的__weakref__是對這個對象的所謂弱引用列表的一個引用,__dict__欄位是對類實例字典的引用,它包含實例屬性的值(注意64位的引用平台會佔用8個位元組)。從Python 3.3開始,共享空間用於在字典中存儲類的所有實例的鍵。這減少了RAM中實例堆棧的大小:
因此,大量的類實例佔用的內存比一個普通字典(dict)佔用的要小:
很容易看出,由於實例字典的大小,RAM中實例的大小仍然很大。
帶有__slots__的類實例
通過消除 __dict__和__weakref__,可以顯著減小RAM中的類實例的大小。這通過一個帶有__slots__的小「技巧」是可能實現的:
RAM中的對象大小明顯變小了:
在類定義中使用__slots__可以顯著減少大量實例對內存空間的佔用:
目前,這是大幅度減少RAM中類實例的內存佔用的主要方法。
這是因為在內存中,對象引用會緊跟標題之後被存儲在內存中——屬性值,並通過類字典中的特殊描述符來訪問它們:
要自動化使用 __slots__創建一個類的過程,有一個庫[namedlist] (https://pypi.org/project/namedlist )可以使用。namedlist.namedlist函數會創建一個帶有__slots__的類:
另一個包[attrs] (https://pypi.org/project/attrs )允許你使用和不使用__slots__自動創建類。
元組
Python還有一個內置的類型tuple(元組),用於表示不可變的數據結構。一個元組是一個固定的結構或記錄,但沒有欄位名。對於欄位訪問,使用的是欄位索引。元組欄位在元組實例創建時就一次性與值對象相關聯:
元組的實例是相當簡潔的:
它們在內存中佔用的位元組比使用__slots__的類實例要多8個位元組,因為內存中的元組跟蹤也包含許多欄位:
Namedtuple(命名元組)
由於元組使用的非常廣泛,某天有人可能會提交一個通過名稱訪問欄位的請求。這個請求的答案是collections.namedtuple模塊。
namedtuple函數的目的是自動生成這樣的類:
它會創建一個元組子類,其中定義了用於按名稱訪問欄位的描述符。在我們的例子中,它看起來是這樣的:
這些類的所有實例都具有與元組相同的內存佔用量。大量的實例會佔用更大的內存空間:
Recordclass: 沒有循環GC的可變namedtuple
由於tuple和相應的namedtuple類會生成不可變對象,因此,ob.x屬性就不能再與另一個值對象相關聯了,對可變namedtuple變體的請求已經出現了。由於Python中沒有與支持賦值的元組相同的內置類型,因此,開發者們創建了許多選項。我們將關注[recordclass] (https://pypi.org/project/recordclass ),它的評級為[stackoverflow] (https://stackoverflow.com/questions/29290359/ exists -of-mutable-name - tuplein -python / 29419745 )。此外,與類元組對象的大小相比,它還可以用來減小RAM中對象的大小。
包recordclass引入了recordclass.mutabletuple類型,它幾乎與tuple相同,但它支持賦值。在此基礎上,創建的子類幾乎與namedtuple完全相同,但它支持將新值賦給欄位(不需要創建新的實例)。recordclass函數與namedtuple函數一樣,允許你自動創建這些類:
只有在沒有PyGC_Head的情況下,類實例才具有與tuple相同的結構:
默認情況下,recordclass函數會創建一個不參與循環垃圾回收機制的類。通常,namedtuple和recordclass用於生成表示記錄或簡單(非遞歸)數據結構的類。在Python中正確使用它們就不會生成循環引用。出於這個原因, 在recordclass生成的類實例後面 ,默認情況下,PyGC_Head 部分會被排除在外, 這對支持循環垃圾回收機制(更準確地說:在與創建的類相關聯的PyTypeObject結構中,默認情況下,flag欄位中的Py_TPFLAGS_HAVE_GC是沒有設置的)的類來說是必要的。
大量實例的內存佔用量比使用了__slots__的類的實例要小:
Dataobject
recordclass庫中提出的另一個解決方案是基於這樣的思想:在內存中使用與帶有__slots__的類實例相同的存儲結構,但不參與循環垃圾回收機制。這些類是使用recordclass.make_dataclass數生成的:
默認情況下,以這種方式創建的類將創建可變實例。
另一種方法——使用繼承自recordclass.dataobject的類聲明:
以這種方式創建的類將創建不參與循環垃圾回收機制的實例。內存中實例的結構與使用__slots__的情況相同,但是沒有PyGC_Head:
為了訪問欄位,還可以使用特殊的描述符通過它從對象開始的偏移量來訪問,這些偏移量位於類字典中:
大量實例內存佔用量的大小在CPython中可能是最小的:
Cython
還有一種基於使用[Cython] (https://cython.org )的方法。它的優點是欄位可以接受C語言原子類型的值。自動創建用於從純Python中來訪問欄位的描述符。例如:
在這種情況下,實例的內存佔用更小:
內存中的實例跟蹤的結構如下:
大量副本的佔用空間要小一些:
但是,請記住,當你從Python代碼訪問時,每次都會執行從int到Python對象的轉換,反之亦然。
Numpy
對大量數據使用多維數組或記錄數組會增加內存佔用。但是,為了在純Python中進行有效的處理,你應該使用那些主要使用了numpy包中的函數的處理方法。
使用函數創建一個由N個元素組成的數組,並將其初始化為0:
內存中數組的大小是可能的最小值:
正常訪問數組元素和行需要將Python對象轉換為C中的 int值,反之亦然。提取單個行會創建一個包含單個元素的數組。它的追蹤就不再那麼簡單了:
因此,如上所述,在Python代碼中,有必要使用numpy包中的函數來處理數組。
結論
通過一個清晰而簡單的示例,可以驗證由開發人員和用戶組成的Python編程語言(CPython)社區確實有可能顯著減少對象使用的內存量。
英文原文:https://habr.com/en/post/458518/
譯者:Nothing
※5千萬的 Twisted 下載量是不會錯的
※基於TensorFlow和Keras的圖像識別(第二部分)
TAG:Python部落 |