Theano教程:Python的內存管理


在寫大型程序時候的一大挑戰是如何保證最少的內存使用率。但是在Python中的內存管理是比較簡單的。Python顯示分配內存,使用引用計數系統管理對象,當指向某一個對象的引用數變為 0 的時候,該對象所占的內存就會被釋放。理論上聽起來很不錯,也很簡單,但是在實踐中,我們需要知道一些Python內存管理的知識從而讓程序在運行過程中能夠更加高效地使用內存。其中一個方面我們需要知道的是基本的Python對象所占空間的大小,另一方面我們需要知道的是Python在內部到底是如何管理內存的。

基本對象

一個 int 對象占多大空間呢? C/C++程序員會說它是由具體的機器決定的,可能是32為或者64位,因此它最多占8個字節(一個字節8位)。那么在Python中也是如此嗎?

下面寫一個函數來揭示出對象占多大的空間(某些情況下需要遞歸,比如某一個對象類型不是基本的數據類型):

 1 import sys  2 
 3 def show_sizeof(x, level=0):  4 
 5     print "\t" * level, x.__class__, sys.getsizeof(x), x  6 
 7     if hasattr(x, '__iter__'):  8         if hasattr(x, 'items'):  9             for xx in x.items(): 10                 show_sizeof(xx, level + 1) 11         else: 12             for xx in x: 13                 show_sizeof(xx, level + 1)

 

我們可以用下面的函數調用來觀察不同的基本數據類型所占空間大小:

show_sizeof(None) show_sizeof(3) show_sizeof(2**63) show_sizeof(102947298469128649161972364837164) show_sizeof(918659326943756134897561304875610348756384756193485761304875613948576297485698417)

 

在64-bit系統和2.7.8 Python上運行的結果:

 <type 'NoneType'> 16 None
 <type 'int'> 24 3
 <type 'long'> 36 9223372036854775808
 <type 'long'> 40 102947298469128649161972364837164
 <type 'long'> 60 918659326943756134897561304875610348756384756193485761304875613948576297485698417

 

可以看到None占了16個字節,int 占了24個字節,是64為系統中C的int64_t 的 3 倍,而且是能夠被機器識別的整型。長整型(無限制的精確度)用來表示出了大於263 - 1的整數,所占空間最小為36個字節。而且這個所占空間大小會隨着算法中整數的大小線性增長。

Python的float是特定實現的,看上去類似於C中double,但是Python中的 float 不會在數據超過8個字節時終止表示:

show_sizeof(3.14159265358979323846264338327950288)

 

在64為系統輸出:

<type 'float'> 24 3.14159265359

 

可以看到又是C中double類型所占空間(8字節)的3倍.

那么對於字符串呢?

show_sizeof("") show_sizeof("My hovercraft is full of eels")

 

在64位系統輸出:

 <type 'str'> 33 
 <type 'str'> 62 My hovercraft is full of eels

 

空字符串占33字節,隨着字符串內容增加,所占空間線性增長。


 

下面測試常用的tuple,list 和 dictionary所占空間大小(均為在64為系統下的輸入結果):

show_sizeof([]) show_sizeof([4, "toaster", 230.1])

 

輸出:

 <type 'list'> 64 [] <type 'list'> 88 [4, 'toaster', 230.1]

 

空list占64個字節,而64位系統中的C++ std::list() 只占16個字節,達到了4倍。

對於tuple呢?dictionary?:

show_sizeof({}) show_sizeof({'a':213, 'b':2131})

 

輸出:

 <type 'dict'> 272 {} <type 'dict'> 272 {'a': 213, 'b': 2131} <type 'tuple'> 64 ('a', 213) <type 'str'> 34 a <type 'int'> 24 213
    <type 'tuple'> 64 ('b', 2131) <type 'str'> 34 b <type 'int'> 24 2131

 

可以看出,對於字典中的每一個 key/value 對,占64字節,但是注意('a', 213)所占空間是64字節,而 'a' 所占空間是34字節,213 占空間是24字節,所以留出64 -(34+24) = 6字節給key/value本身;另外,我們看到整個字典占272字節,而不是64+64 = 128字節。字典本身是被設計成一個搜索效率高的數據結構,所以會用到必要的額外的空間。如果字典內部采用的是某種樹結構,必須考慮到包含每一個值的節點和指向孩子節點的兩個指針的空間消耗;如果字典內部采用哈希表實現,我們必須保證有足夠的空閑空間從而保證性能。

字典與C++std::map結構對等,而C++的map在創建(空map)時占48個字節, C++空字符串占 8 個字節,整數占4個字節。

觀察到了這么多現象,到底是怎么回事?看上去一個空字符串占8個字節還是占37個字節似乎改變不了什么。如果不擴展數據大小,確實如此。我們必須關心的是我們創建多少個對象會到達程序所使用的內存的限制。在實踐應用中,這個問題很棘手。要想設計出一個管理內存的好策略,不但需要關心對象所占內存的大小,還需要所創建對象的數量以及這些對象的創建順序,事實證明這對於Python很重要。一個關鍵元素就是理解Python是如何在內部分配內存的,也正是下面即將討論的.

內部內存管理

為了加速內存分配(和重復使用),Python對小型對象使用列表來管理。每個列表包含的對象所占空間大小都很相近:如一個列表包含的對象均占1到8個字節,另一個列表包含的對象均占9到16個字節等。當需要創建一個小型對象時,要么重復使用列表中空閑塊,要么分配一塊新空間。

事實上,即使一個對象的空間被free了,它做占據的內存空間也不會被返回給Python的全局內存池,而是僅僅被標記為free然后加入到空閑列表。過期的(被消亡)對象的位置空間會在一個新的差不多大小的對象被創建時,進行重復使用,如果沒有過期的對象釋放的空間存在,那么就直接新分配空間。

如果小型對象的所占內存從未被釋放,那么列表所占內存空間就會一直增大,那么內存慢慢就會被這些大量的小型對象占據。

因此,我們應該努力只分配空間給那些有必要的對象,在循環中只創建少量的對象,盡量使用生成器語法。

事實上,列表占據空間的自由增長似乎並不算是一個問題,因為列表所包含的空間仍然允許Python程序進入和使用。但是從操作系統的視角來看,程序所占內存的大小會超過系統分配給Python的總內存的大小。

為了證明上面所述,使用memory_profiler(依賴於 python-psutil包)來證明:

 1 import copy  2 import memory_profiler  3 
 4 #這里加上@profile是來監視具體函數function的內存使用情況
 5 @profile  6 def function():  7     x = list(range(1000000))  # allocate a big list
 8     y = copy.deepcopy(x)  9     del x 10     return y 11 
12 if __name__ == "__main__": 13     function()

 

在Ubuntu上運行:

 

程序創建了包含1,000,000個int值(1,000,000*12 bytes = ~11.4MB),建立一個對list的引用變量x(1,000,000 * 8 bytes =~ 3.8MB), 總內存使用量大約為15.2MB.然后copy.deepcopy 進行深度拷貝操作和建立新的引用變量y,同樣需要占用內存大約15.2MB,所以第8行的內存使用量增加了15.367MB. 注意第 9 行,del x, 內存使用量僅僅減少了3.824MB,這表明del操作只是釋放了指向 list 引用變量的內存空間,而不是list中的整數所占內存空間,這些整數值保留在堆中,導致內存占用多了將近11.4MB.

在這個例子中分配了總共大約15.309 + 15.367 - 3.82 = ~26.8MB, 而我們存儲一個list只需要大約11.4MB的內存,超出了1倍多! 所以,在編程中的也許我們不注意的地方,就會導致內存占用增長很快!

pickle

pickle是一種標准的把Python對象序列化到文件和以及從文件解序列化出來的方式。它的內存足跡(memory footprint)是什么? 它創建了額外的數據副本還是用一種更加聰明的方式?考慮:

 1 import memory_profiler  2 import pickle  3 import random  4 
 5 def random_string():  6     return "".join([chr(64 + random.randint(0, 25)) for _ in xrange(20)])  7 
 8 @profile  9 def create_file(): 10     x = [(random.random(), 11  random_string(), 12           random.randint(0, 2 ** 64)) 13          for _ in xrange(1000000)] 14 
15     pickle.dump(x, open('machin.pkl', 'w')) 16 
17 @profile 18 def load_file(): 19     y = pickle.load(open('machin.pkl', 'r')) 20     return y 21 
22 if __name__=="__main__": 23  create_file() 24     #load_file()

 

這個程序用來生成一些pickle 數據和讀取pickle 數據(pickle數據的讀取在這里注釋了,首先沒用讓讀取函數運行),使用memory_profiler,生成pickle數據過程中使用了大量內存:

再看看pickle數據的讀取(把上面程序中第23行注釋掉,把24行的注釋去掉):

所以,pickle是非常消耗內存的做法,從上面的圖看出,在數據的創建時,大約使用127MB內存,而一個pickle.dump操作就要額外使用差不多與數據相當的內存空間(117MB).

在unpickle操作中(即反序列化操作,從pkl中讀取數據),看上去效率還好點,雖然確實占用了比原始數據(127MB)大的內存空間(188MB),但是還沒到達有超1倍的程度。

總之,涉及pickle的操作應該在對內存容量要求較高的程序中盡量避免。那么,有沒有可以替代的選擇呢?我們知道pickle保存了數據結構的結構,即將數據原封不動保存起來(不僅僅保存數據,還要保存數據的結構信息),所以我們才能在需要的時候,將數據從pickle文件中恢復出來。但是,並不是所有時候都需要這樣用pickle保存,就像上面例子中的list,完全可以用一個基於文本的文件格式按順序保存里面的元素,沒必要用pickle來保存:

 1 import memory_profiler  2 import random  3 import pickle  4 
 5 def random_string():  6     return "".join([chr(64 + random.randint(0, 25)) for _ in xrange(20)])  7 
 8 @profile  9 def create_file(): 10     x = [(random.random(), 11  random_string(), 12           random.randint(0, 2 ** 64)) 13          for _ in xrange(1000000) ] 14     # 這里使用文本來保存數據而不是pickle
15     f = open('machin.flat', 'w') 16     for xx in x: 17         print >>f, xx 18  f.close() 19 
20 @profile 21 def load_file(): 22     y = [] 23     f = open('machin.flat', 'r') 24     for line in f: 25  y.append(eval(line)) 26  f.close() 27     return y 28 
29 if __name__== "__main__": 30  create_file() 31     #load_file()

 

建立文件時,內存足跡:

與上面pickle保存數據對比,可以發現,通過文本保存文件值占用幾乎可以忽略的內存。

下面再來看看數據的讀取時,內存足跡變化(將30行的代碼注釋,將31行的注釋符去掉):

原始數據127MB,讀取時占用內存139MB,和原始數據很接近,多出來的約10MB內存空間是分配給循環中產生的臨時變量。

這個例子可以啟示我們在處理數據的時候不要首先全部讀取數據,然后再處理數據,而是每次讀取幾項,處理完這幾項,釋放這幾項的空間,然后再讀取幾項處理,以此類推,這樣,之前分配過的內存空間就可以重復使用。比如讀取數據到一個Numpy的array中,我們可以先創建一個空array,然后逐行讀取數據,逐行填入array,這樣大約只需要和數據大小差不多的內存空間。如果使用pickle, 至少要分配2倍於數據大小的內存空間:一次是pickle在load時分配占用,一次是創建存儲數據的array.

總結

Python 設計的目標根本上就不同於 C 語言設計的目標。后者是以更加復雜和顯示的編程為代價讓程序員能夠更好地控制程序要做的事,而前者設計的目的是讓代碼更加迅速並且盡量隱藏細節。盡管聽起來不錯,但是在生產環境中,忽略執行效率會栽大跟頭,所以在Python代碼設計過程中,知道哪些代碼執行的效率很低,從而盡量避免這種低效率編寫對於生產環境來說很重要!

 

 資料來源:http://deeplearning.net/software/theano/tutorial/python-memory-management.html#python-memory-management


免責聲明!

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



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