Python內存優化:Profile,slots,compact dict


  實際項目中,pythoner更加關注的是Python的性能問題,之前也寫過一篇文章《Python性能優化》介紹Python性能優化的一些方法。而本文,關注的是Python的內存優化,一般說來,如果不發生內存泄露,運行在服務端的Python代碼不用太關心內存,但是如果運行在客戶端(比如移動平台上),那還是有優化的必要。具體而言,本文主要針對的Cpython,而且不涉及C擴展。  

  我們知道,Python使用引用技術和垃圾回收來管理內存,底層也有各種類型的內存池,那我們怎么得知一段代碼使用的內存情況呢?工欲善其事必先利其器,直接看windows下的任務管理器或者linux下的top肯定是不准的。

  本文地址:http://www.cnblogs.com/xybaby/p/7488216.html

Pytracemalloc

  對於基本類型,可以通過sys.getsizeof()來查看對象占用的內存大小。以下是在64位Linux下的一些結果:

>>> import sys

>>> sys.getsizeof(1)
24
>>> sys.getsizeof([])
72
>>> sys.getsizeof(())
56
>>> sys.getsizeof({})
280
>>> sys.getsizeof(True)
24

  可以看到,即使是一個int類型(1)也需要占用24個字節,遠遠高於C語言中int的范圍。因為Python中一切都是對象,int也不例外(事實上是PyIntObject),除了真正存儲的數值,還需要保存引用計數信息、類型信息,更具體的可以參見《Python源碼剖析》。

   而對於更復雜的組合類型,復雜的代碼,使用getsizeof來查看就不准確了,因為在Python中變量僅僅指向一個對象,這個時候就需要更高級的工具,比如guppypysizerpytracemallocobjgraph。在這里重點介紹pytracemalloc。

 

  在Python3.4中,已經支持了pytracemalloc,如果使用python2.7版本,則需要對源碼打補丁,然后重新編譯。pytracemalloc在pep454中提出,主要有以下幾個特點:

  • Traceback where an object was allocated
  • Statistics on allocated memory blocks per filename and per line number: total size, number and average size of allocated memory blocks
  • Compute the differences between two snapshots to detect memory leaks

  簡單來說,pytracemalloc hook住了python申請和釋放內存的接口,從而能夠追蹤對象的分配和回收情況。對內存分配的統計數據可以精確到每個文件、每一行代碼,也可以按照調用棧做聚合分析。而且還支持快照(snapshot)功能,比較兩個快照之間的差異可以發現潛在的內存泄露。

 

  下面通過一個例子來簡單介紹pytracemalloc的用法和接口,關於更詳細用法和API,可以參考這份詳盡的文檔或者pytracemalloc的作者在pycon上的演講ppt

 1 import tracemalloc
 2 
 3 NUM_OF_ATTR =  10
 4 NUM_OF_INSTANCE = 100
 5  
 6 class Slots(object):
 7     __slots__ = ['attr%s'%i for i in range(NUM_OF_ATTR)]
 8     def __init__(self):
 9         value_lst = (1.0, True, [], {}, ())
10         for i in range(NUM_OF_ATTR):
11             setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])
12 
13 
14 class NoSlots(object):
15     def __init__(self):
16         value_lst = (1.0, True, [], {}, ())
17         for i in range(NUM_OF_ATTR):
18             setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])
19 
20 
21 
22 def generate_some_objs():
23     lst = []
24     for i in range(NUM_OF_INSTANCE):
25         o = Slots() if i % 2 else NoSlots()
26         lst.append(o)
27     return lst
28 
29 
30 if __name__ == '__main__':
31     tracemalloc.start(3)
32 
33     t = generate_some_objs() 
34 
35     snapshot = tracemalloc.take_snapshot()
36     top_stats = snapshot.statistics('lineno') # lineno filename traceback
37 
38     print(tracemalloc.get_traced_memory())
39     for stat in top_stats[:10]:
40         print(stat)  

 

  在上面的代碼中,用到了pytracemalloc幾個核心的API:

  start(nframe: int=1)  

    pytracemalloc的一大好處就是可以隨時啟停,start函數即開始追蹤內存分配,相應的stop會停止追蹤。start函數有一個參數,nframes : 內存分配時記錄的棧的深度,這個值越大,pytracemalloc本身消耗的內存越多,在計算cumulative數據的時候有用。

  get_traced_memory()

    返回值是擁有兩個元素的tuple,第一個元素是當前分配的內存,第二個元素是自內存追蹤啟動以來的內存峰值。

  take_snapshot()    

    返回當前內存分配快照,返回值是Snapshot對象,該對象可以按照單個文件、單行、單個調用棧統計內存分配情況    

 

  運行環境:windows 64位python3.4

(62280, 62920)

test.py:10: size=16.8 KiB, count=144, average=120 B
test.py:17: size=16.7 KiB, count=142, average=120 B
test.py:19: size=9952 B, count=100, average=100 B
test.py:26: size=9792 B, count=102, average=96 B
test.py:27: size=848 B, count=1, average=848 B
test.py:34: size=456 B, count=1, average=456 B
test.py:36: size=448 B, count=1, average=448 B
D:\Python3.4\lib\tracemalloc.py:474: size=64 B, count=1, average=64 B

  如果將第36行的“lineno“改成“filename”,那么結果如下

(62136, 62764)

test.py:0: size=54.5 KiB, count=491, average=114 B
D:\Python3.4\lib\tracemalloc.py:0: size=64 B, count=1, average=64 B

   

  有了Profile結果之后,可以看出來在哪個文件中有大量的內存分配。與性能優化相同,造成瓶頸的有兩種情況:單個對象占用了大量的內存;同時大量存在的小對象。對於前者,優化的手段並不多,惰性初始化屬性可能有一些幫助;而對於后者,當同樣類型的對象大量存在時,可以使用slots進行優化。

Slots

  默認情況下,自定義的對象都使用dict來存儲屬性(通過obj.__dict__查看),而python中的dict大小一般比實際存儲的元素個數要大(以此降低hash沖突概率),因此會浪費一定的空間。在新式類中使用__slots__,就是告訴Python虛擬機,這種類型的對象只會用到這些屬性,因此虛擬機預留足夠的空間就行了,如果聲明了__slots__,那么對象就不會再有__dict__屬性。

  使用slots到底能帶來多少內存優化呢,首先看看這篇文章,對於一個只有三個屬性的Image類,使用__slots__之后內存從25.5G下降到16.2G,節省了9G的空間!

  

  到底能省多少,取決於類自身有多少屬性、屬性的類型,以及同時存在多少個類的實例。下面通過一段簡單代碼測試一下:

 1 # -*- coding: utf-8 -*-
 2 import sys
 3 import tracemalloc
 4 
 5 NUM_OF_ATTR =  3 #3 # 10 # 30 #90
 6 NUM_OF_INSTANCE = 10 # 10 # 100
 7  
 8 class Slots(object):
 9     __slots__ = ['attr%s'%i for i in range(NUM_OF_ATTR)]
10     def __init__(self):
11         value_lst = (1.0, True, [], {}, ())
12         for i in range(NUM_OF_ATTR):
13             setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])
14 
15 
16 class NoSlots(object):
17     def __init__(self):
18         value_lst = (1.0, True, [], {}, ())
19         for i in range(NUM_OF_ATTR):
20             setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])
21 
22 if __name__ == '__main__':
23     clz = Slots if len(sys.argv) > 1 else NoSlots
24     tracemalloc.start()
25     objs = [clz() for i in range(NUM_OF_INSTANCE)]
26     print(tracemalloc.get_traced_memory()[0])

  

  上面的代碼,主要是在每個實例的屬性數目、並發存在的實例數目兩個維度進行測試,並沒有測試不同的屬性類型。結果如下表:

  

  百分比為內存優化百分比,計算公式為(b - a) / b, 其中b為沒有使用__slots__時分配的內存, a為使用了__slots__時分配的內存。

  

注意事項

  關於__slots__,Python文檔有非常詳盡的介紹,這里只強調幾點注意事項

  第一:基類和子類都必須__slots__,即使基類或者子類沒有屬性

>>> class Base(object):

...     pass
...
>>> class Derived(Base):
...     __slots__ = ('a', )
...
>>> d.__slots__
('a',)
>>> getattr(d, '__dict__', 'No Dict')
{}

  從上面的示例可以看到,子類的對象還是有__dict__屬性,原因就在於基類沒有聲明__slots__。因此,可以通過看子類的實例有沒有__dict__屬性來判斷slots的使用是否正確

 

  第二:子類會繼承基類的__slots__

  更准確的說,如果訪問屬性的時候沒有在子類的__slots__找到,會繼續在基類的__slots__查找,因為Python使用descriptor在類這個層級實現__slots__的,具體可以參見《 python屬性查找 深入理解》一文

>>> class Base(object):

...     __slots__ = ('a',)
...
>>> class Derived(Base):
...     __slots__ = ('b', )
...
>>> d = Derived()
>>> d.__slots__
('b',)
>>> getattr(d, '__dict__', 'No Dict')
'No Dict'
>>> d.a = 1
>>> d.c = 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Derived' object has no attribute 'c'

 

objgraph

  在大型工程中,怎么排查有哪些大量存在的對象呢,畢竟同一個類型存在的對象越多,優化越有效果。除了直接看代碼,最好使的就是使用objgraph.py的show_most_common_types(N)函數,該函數返回Python gc管理的所有對象中,數目前N多的對象,在排除掉python builtin對象之后,剩下的就是可優化的對象。比如在最上面的代碼中:在最后加上這么兩句:

import objgraph

objgraph.show_most_common_types(25)

  輸出如下:

  

 

再論Python dict

  前面介紹slots的時候,就提到Python自定義的對象中通過dict來管理屬性。這種機制極大的提高了Python的靈活性 -- 可以隨時給對象增加屬性,但是其實現機制也帶來了內存上的浪費。不管是python源碼,還是Python程序,都大量使用了dict,因此這部分內存浪費不容小視。

  python中的dict使用的是散列表(類似C++中的std::unordered_map),當計算出的hash值沖突的時候,采用開放地址法解決沖突(另一種常見的沖突解決算法是鏈表法)。為了降低沖突概率,當裝填因子(實際存儲的元素與散列表長度的比值)超過2/3的時候就會對散列表進行擴容,因此散列表中一定會存在一些未使用的槽。

  下面簡單看看PyDictObject的數據結構(python2.7.3 dictobject.h)

 1 #define PyDict_MINSIZE 8
 2 
 3 typedef struct {
 4     /* Cached hash code of me_key.  Note that hash codes are C longs.
 5      * We have to use Py_ssize_t instead because dict_popitem() abuses
 6      * me_hash to hold a search finger.
 7      */
 8     Py_ssize_t me_hash;
 9     PyObject *me_key;
10     PyObject *me_value;
11 } PyDictEntry;
12 
13 
14 typedef struct _dictobject PyDictObject;
15 struct _dictobject {
16     PyObject_HEAD
17     Py_ssize_t ma_fill;  /* # Active + # Dummy */
18     Py_ssize_t ma_used;  /* # Active */
19 
20     /* The table contains ma_mask + 1 slots, and that's a power of 2.
21      * We store the mask instead of the size because the mask is more
22      * frequently needed.
23      */
24     Py_ssize_t ma_mask;
25 
26     /* ma_table points to ma_smalltable for small tables, else to
27      * additional malloc'ed memory.  ma_table is never NULL!  This rule
28      * saves repeated runtime null-tests in the workhorse getitem and
29      * setitem calls.
30      */
31     PyDictEntry *ma_table;
32     PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
33     PyDictEntry ma_smalltable[PyDict_MINSIZE];
34 };

   從定義可以看出,除了固定的部分(幾個Py_ssize_t),PyDictObject中主要是PyDictEntry對象,PyDictEntrty包含一個Py_ssize_t(int)和兩個指針。上面源碼中的注釋(第26行)指出,當dict的元素比較少時,ma_table指向ma_smalltable,當元素增多時,ma_table會指向新申請的空間。ma_smalltable的作用在於Python(不管是源碼還是代碼)都大量使用dict,一般來說,存儲的元素也不會太多,因此Python就先開辟好PyDict_MINSIZE(默認為8)個空間。

  為什么說PyDictObject存在浪費呢,PyDictEntry在32位下也有12個字節,那么即使在ma_smalltable(ma_table)中大量的位置沒有被使用時,也要占用這么多字節。用這篇文章中的例子:

  假設有這么一個dict:   d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}

  在Python源碼中的視圖就是這樣的:

 #  下面的entries就是ma_smalltable

entries = [['--', '--', '--'],
           [-8522787127447073495, 'barry', 'green'],
           ['--', '--', '--'],
           ['--', '--', '--'],
           ['--', '--', '--'],
           [-9092791511155847987, 'timmy', 'red'],
           ['--', '--', '--'],
           [-6480567542315338377, 'guido', 'blue']]

   然而,完全可以這么存儲:

 indices = [None, 1, None, None, None, 0, None, 2]

entries =  [[-9092791511155847987, 'timmy', 'red'],
                [-8522787127447073495, 'barry', 'green'],
                [-6480567542315338377, 'guido', 'blue']]

  indices的作用類似ma_smalltable,但只存儲一個數組的索引值,數組只存儲實際存在的元素(PyDictEntry),當dict中的元素越稀疏,相比上一種存儲方式使用的內存越少。而且,這種實現, dict就是有序的(按插入時間排序)

  這就是python3.6中新的dict實現,Compact dict! Stackoverflow上也有相關討論

總結

   本文中介紹了Python內存優化的Profile工具,最有效的優化方法:使用slots,也介紹了在python3.6中新的dict實現。

  當然,還有一些良好的編碼習慣。比如盡量使用immutable而不是mutable對象:使用tuple而不是list,使用frozenset而不是set;另外,就是盡量使用迭代器,比如python2.7中,使用xrange而不是range,dict的iterxx版本。

references

pytracemalloc

pep454: Add a new tracemalloc module to trace Python memory allocations  

save-ram-with-python-slots/

Python源碼分析-PyDictObject

More compact dictionaries with faster iteration


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM