作者:Gigi Sayfan
翻譯:吳振東
校對:吳金笛
本文約3300字,建議閱讀10分鐘。
本文將介紹多種Python對象分別所佔用的內存,並解釋所選擇的測量方法和函數,為節省內存提供建議。
Python是一種很棒的程式語言。不過它的運行速度很慢,這是由於它具有極大的靈活性和動態特徵所造成的。對於許多應用和領域來說,考慮到它們的要求和各種優化技術,這並不能算是一個問題。眾所周知,Python對象圖(列表、元組和基元類型的嵌套字典)佔用了大量內存。這可能是一個更為嚴格的限制因素,因為這對緩存、虛擬內存、與其他程序的多租戶產生了影響,而且通常會更快地耗盡一種稀缺且昂貴的資源——可用內存。
事實證明,想要弄清楚實際消耗了多少內存並非易事。在本文中,我將向你介紹Python對象內存管理的複雜性,並展示如何準確地去測量所消耗的內存。
在本文中,我只關注CPython——Python程式語言的主要實現。這裡的實驗結論並不適用於其他Python的實現,例如IronPython,Jython和PyPy。
另外,我是在Python 2.7上運行所得到的這些數據。如果是在Python 3中,這些結果可能會略有不同(特別是對於Unicode的字符串),但是理念是基本相同的。
關於Python內存使用的實踐探索
首先,讓我們初步探索一下,來了解Python對象的實際內存使用的具體情況。
內嵌函數sys.getsizeof()
標準庫的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對象的內存
首先從數值類型開始:
```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對象具有巨大的固定開銷。如果你的數據結構由大量的集合對象組成,比如說字符串、列表和字典,每個集合都包含少量的項,你同樣要為之付出沉重的代價。
deep_getsizeof()函數
現在你可能被我上面所提到的嚇出一身冷汗,這同時也證明了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字符串)的引用,但會把每一個引用的指針計算在內。
處理方式or騙招
事實證明,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 ```
Python內存vs系統內存
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為它的對象使用了大量內存,也使用了各種技巧和優化方式來進行內存管理。通過跟蹤對象的內存使用情況並了解內存管理模型,可以顯著減少程序的內存佔用。
學習Python,無論你是剛入門的新手還是經驗豐富的編碼人員,都可以使用我們的完整Python教程指南來學習。
原文標題:
Understand How Much Memory Your Python Objects Use
原文連結:
https://code.tutsplus.com/tutorials/understand-how-much-memory-your-python-objects-use--cms-25609
編輯:王菁
校對:林亦霖
吳振東,法國洛林大學計算機與決策專業碩士。現從事人工智慧和大數據相關工作,以成為數據科學家為終生奮鬥目標。來自山東濟南,不會開挖掘機,但寫得了Java、Python和PPT。
工作內容:需要一顆細緻的心,將選取好的外文文章翻譯成流暢的中文。如果你是數據科學/統計學/計算機類的留學生,或在海外從事相關工作,或對自己外語水平有信心的朋友歡迎加入翻譯小組。
你能得到:定期的翻譯培訓提高志願者的翻譯水平,提高對於數據科學前沿的認知,海外的朋友可以和國內技術應用發展保持聯繫,THU數據派產學研的背景為志願者帶來好的發展機遇。
其他福利:來自於名企的數據科學工作者,北大清華以及海外等名校學生他們都將成為你在翻譯小組的夥伴。
點擊文末「閱讀原文」加入數據派團隊~
轉載須知
如需轉載,請在開篇顯著位置註明作者和出處(轉自:數據派ID:datapi),並在文章結尾放置數據派醒目二維碼。有原創標識文章,請發送【文章名稱-待授權公眾號名稱及ID】至聯繫郵箱,申請白名單授權並按要求編輯。
發布後請將連結反饋至聯繫郵箱(見下方)。未經許可的轉載以及改編者,我們將依法追究其法律責任。
點擊「閱讀原文」擁抱組織