本文將介紹多種Python對象分別所佔用的內存,並解釋所選擇的測量方法和函數,爲節省內存提供建議。python
Python是一種很棒的編程語言。不過它的運行速度很慢,這是因爲它具備極大的靈活性和動態特徵所形成的。對於許多應用和領域來講,考慮到它們的要求和各類優化技術,這並不能算是一個問題。衆所周知,Python對象圖(列表、元組和基元類型的嵌套字典)佔用了大量內存。這多是一個更爲嚴格的限制因素,由於這對緩存、虛擬內存、與其餘程序的多租戶產生了影響,並且一般會更快地耗盡一種稀缺且昂貴的資源——可用內存。編程
事實證實,想要弄清楚實際消耗了多少內存並不是易事。在本文中,我將向你介紹Python對象內存管理的複雜性,並展現如何準確地去測量所消耗的內存。緩存
在本文中,我只關注CPython——Python編程語言的主要實現。這裏的實驗結論並不適用於其餘Python的實現,例如IronPython,Jython和PyPy。bash
另外,我是在Python 2.7上運行所獲得的這些數據。若是是在Python 3中,這些結果可能會略有不一樣(特別是對於Unicode的字符串),可是理念是基本相同的。數據結構
首先,讓咱們初步探索一下,來了解Python對象的實際內存使用的具體狀況。app
標準庫的sys模塊提供了getsizeof()函數。該函數接收一個對象(和可選的默認值),調用sizeof()方法並返回結果,從而可讓你所使用的對象具有可檢查性。編程語言
getsizeof()
https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10&token=1853049065&lang=zh_CN#sys.getsizeof複製代碼
首先從數值類型開始:函數
python import sys
sys.getsizeof(5) 24
複製代碼
有意思,一個整數(integer)佔用了24字節。性能
python sys.getsizeof(5.3) 24複製代碼
嗯……一個浮點數(float)一樣佔用24字節。優化
python from decimal import Decimal sys.getsizeof(Decimal(5.3)) 80
哇哦,80字節!如此一來你可能要想想是該用float仍是Decimals來表示大量的實數了。
讓咱們看一下字符串(strings)和collections:
python sys.getsizeof(‘’) 37 sys.getsizeof(‘1’) 38 sys.getsizeof(‘1234’) 41
sys.getsizeof(u’’) 50 sys.getsizeof(u’1’) 52 sys.getsizeof(u’1234’) 58複製代碼
好吧。一個空字符串佔用37字節,每增長一個字符就增長1個字節。這提出了一個關於對保留多個短字符串的權衡問題,你是願意爲每一個短字符串支付37字節的開銷,仍是願意爲一個長字符串一次性地支付開銷。
Unicode字符串的行爲相似,但它的開銷是50字節,每增長一個字符就會增長2字節的開銷。若是你使用返回Unicode字符串的庫,而你的文本本來能夠用簡單的字符串來表示的話,那麼你就須要考慮下這一點。
順便說一下,在Python 3中,字符串都是Unicode,開銷是49字節(它們在某處節省了1字節)。Bytes對象的開銷是33字節。若是你的程序在內存中須要處理大量的短字符串,而你又很關心程序的性能的話,那麼建議你考慮使用Python 3。
python sys.getsizeof([]) 72 sys.getsizeof([1]) 88 sys.getsizeof([1, 2, 3, 4]) 104 sys.getsizeof(['a long longlong string'])複製代碼
這是怎麼回事?一個空的list佔用72字節,但每增長一個int只加大了8字節,其中一個int佔用24字節。一個包含長字符串的list只佔用80字節。
答案其實很簡單。list並不包含int對象自己。它只包含一個佔8字節(在CPython 64位版本中)指向實際int對象的指針。這意味着getsizeof()函數不返回list的實際內存及其包含的全部對象,而只返回list的內存和指向其對象的指針。
在下一節中,我將介紹能夠解決此問題的deep_getsizeof()函數。
python sys.getsizeof(()) 56 sys.getsizeof((1,)) 64 sys.getsizeof((1, 2, 3, 4)) 88 sys.getsizeof(('a long longlong string',)) 64複製代碼
對於元組(tuples)來講狀況相似。空元組的開銷是56字節,空list是72字節。若是你的數據結構包括許多小的不可變的序列,那麼每一個序列之間所差的這16字節是一個很是容易實現的目標。
python sys.getsizeof(set()) 232 sys.getsizeof(set([1)) 232 sys.getsizeof(set([1, 2, 3, 4])) 232
sys.getsizeof({}) 280 sys.getsizeof(dict(a=1)) 280 sys.getsizeof(dict(a=1, b=2, c=3)) 280
複製代碼
當你添加一個項時,集合(Set)和字典(dictionary)在表面上根本不會有所增加,但請注意它們所帶來的巨大開銷。
緣由是Python對象具備巨大的固定開銷。若是你的數據結構由大量的集合對象組成,好比說字符串、列表和字典,每一個集合都包含少許的項,你一樣要爲之付出沉重的代價。
如今你可能被我上面所提到的嚇出一身冷汗,這同時也證實了sys.getsizeof()只能告訴你原始對象須要多少內存,那麼讓咱們來看一種更合適的解決方案。
deep_getsizeof()是向下層遞歸的函數,而且能夠計算Python對象圖的的內存實際使用量。
python from collections import Mapping, Container from sys import getsizeof
def deep_getsizeof(o, ids): 「"」Find the memory footprint of a Python object複製代碼
這是一個遞歸函數,它向下讀取一個Python對象圖,好比說一個包含列表套用列表的嵌套字典的字典和元組以及集合。
sys.getsizeof函數僅執行較淺的深度。無論它的容器內的每一個對象的實際大小,它都將其設爲指針。
:param o: the object
:param ids:
:return:
""" d = deep_getsizeof if id(o) in ids: return 0 r = getsizeof(o) ids.add(id(o)) if isinstance(o, str) or isinstance(0, unicode): return r if isinstance(o, Mapping): return r + sum(d(k, ids) + d(v, ids) for k, v in o.iteritems()) if isinstance(o, Container): return r + sum(d(x, ids) for x in o) return r 複製代碼
對於這個函數來講有幾個有趣的方面。它會考慮屢次引用的對象,並經過追蹤對象ID來對它們進行一次計數。這一實現的另外一個有趣的特性是它充分利用了collections模塊的抽象基類。這使得這個函數能夠很是簡潔地處理任何實現Mapping和Container基類的集合,而不是直接去處理無數集合類型,例如:字符串、Unicode、字節、列表、元組、字典、frozendict, OrderedDict, 集合、 frozenset等等。
讓咱們看下它是如何執行的:
python x = '1234567' deep_getsizeof(x, set()) 44複製代碼
一個長度爲7的字符串佔用了44字節(原開銷37字節+7個字符佔用7字節)。
python deep_getsizeof([], set()) 72複製代碼
空列表佔用72字節(只有原開銷)。
python deep_getsizeof([x], set()) 124複製代碼
一個包含字符串x的列表佔用124字節(72+8+44)。
python deep_getsizeof([x, x, x, x, x], set()) 156
一個包含5個x字符串的列表佔用156字節(72+5*8+44)。
最後一個例子顯示了deep_getsizeof()只計算一次同一對象(x字符串)的引用,但會把每個引用的指針計算在內。
事實證實,CPython中有一些騙招,因此你從deep_getsizeof()中所獲得的數字並不能徹底表明Python程序中的內存使用。
Python使用引用計數語義來管理內存。一旦對象再也不被使用,就會釋放其內存。但只要存在引用,該對象就不會被釋放。那些循環引用之類的東西會讓你感到很難受。
CPython能夠管理8字節邊界上的特殊池裏的小對象(小於256字節)。有1-8字節的池,9-16字節的池,一直到249-256字節的池。當一個10字節大小的對象被分配時,它會從16字節池中分配出大小爲9-16字節的對象。所以,即使他只包含10字節的數據,但它仍是會花費16字節的內存。若是1,000,000個10字節大小的對象被分配時,實際使用的內存是16,000,000字節,而不是10,000,000個字節。這其中多出的60%的開銷顯然是微不足道的。
CPython保留了【-5,256】範圍內全部整數的全局列表。這種優化策略是頗有意義的,由於小整數隨時隨地均可能會出現。假設每一個整數佔用24個字節,那麼這就會爲典型的程序節省大量內存。
這意味着CPython爲全部這些整數都預先分配了266*24=6384個字節,即使它們中的大部分你用不到。你可使用id()函數來驗證它,這個函數提供指向實際函數的指針。若是對【-5,256】範圍內的任意x屢次調用id(x),那麼每次都會獲得相同的結果(對於相同的整數)。但若是你拿超出這個範圍的整數作嘗試,那麼每次獲得的結果都不相同(每次都會動態創造一個新的對象)。
這有幾個在這個範圍內的例子:
python id(-3) 140251817361752
id(-3) 140251817361752
id(-3) 140251817361752
id(201) 140251817366736
id(201) 140251817366736
id(201) 140251817366736 複製代碼
這有幾個超過這個範圍的例子:
python id(301) 140251846945800
id(301) 140251846945776
id(-6) 140251846946960
id(-6) 140251846946936 複製代碼
CPython具備一種所屬性。在不少狀況下,當程序中的內存對象再也不被引用時,他們不會再返回系統中(例如小對象)。若是你分配和釋放許多對象(屬於同一個8字節池的),這會對你的程序頗有好處,由於不須要去打擾系統,不然代價會是很是昂貴的。不過若是你的程序一般在使用X字節並在偶然狀況下使用它100次(例如僅在啓動時解析和處理大配置文件),那麼效果就不是特別好了。
如今,100X的內存有可能被毫無用處的困在你的程序裏,永遠不會被再次利用,並且也拒絕被系統分配給其餘程序。更具諷刺意義的是,若是你使用處理模塊來運行程序的多個實例,那麼就會嚴重限制你在給定計算機上能夠運行的實例數。
想要衡量和測量程序的實際內存使用狀況,可使用memory_profiler模塊。我嘗試了一下,不肯定所得出的結果是否可信。它使用起來很是簡單。你裝飾一個函數(多是@profiler裝飾器的主函數0函數),當程序退出時,內存分析器會打印出一份標準輸出的簡潔報告,顯示每行的總內存和內存變化。我是在分析器下運行的這個示例。
memory_profiler
https://pypi.python.org/pypi/memory_profiler
python from memory_profiler import profile
@profile def main(): a = [] b = [] c = [] for i in range(100000): a.append(5) for i in range(100000): b.append(300) for i in range(100000): c.append(‘123456789012345678901234567890’) del a del b del c
print 'Done!' if __name__ == '__main__':
main()
Here is the output:
Line # Mem usage Increment Line Contents
================================================
3 22.9 MiB 0.0 MiB @profile
4 def main():
5 22.9 MiB 0.0 MiB a = []
6 22.9 MiB 0.0 MiB b = []
7 22.9 MiB 0.0 MiB c = []
8 27.1 MiB 4.2 MiB for i in range(100000):
9 27.1 MiB 0.0 MiB a.append(5)
10 27.5 MiB 0.4 MiB for i in range(100000):
11 27.5 MiB 0.0 MiB b.append(300)
12 28.3 MiB 0.8 MiB for i in range(100000):
13 28.3 MiB 0.0 MiB c.append('123456789012345678901234567890')
14 27.7 MiB -0.6 MiB del a
15 27.9 MiB 0.2 MiB del b
16 27.3 MiB -0.6 MiB del c
17
18 27.3 MiB 0.0 MiB print 'Done!'複製代碼
如你所見,這裏的內存開銷是22.9MB。在【-5,256】範圍內外添加整數和添加字符串時內存不增長的緣由是在全部狀況下都使用單個對象。目前尚不清楚爲何第8行的第一個range(1000)循環增長了4.2MB,而第10行的第二個循環只增長了0.4MB,第12行的第三個循環增長了0.8MB。最後,當刪除a,b和C列表時,爲a和c釋放了0.6MB,可是爲b添加了0.2MB。對於這些結果我並非特別理解。
CPython爲它的對象使用了大量內存,也使用了各類技巧和優化方式來進行內存管理。經過跟蹤對象的內存使用狀況並瞭解內存管理模型,能夠顯著減小程序的內存佔用。
文章來自阿里雲開發者社區