零、預備知識
在Python中,列表是一個動態的指針數組,而array模塊所提供的array對象則是保存相同類型的數值的動態數組。由於array直接保存值,因此它所使用的內存比列表少。列表和array都是動態數組,因此往其中添加新元素,而沒有空間保存新的元素時,它們會自動重新分配內存塊,並將原來的內存中的值復制到新的內存塊中。為了減少重新分配內存的次數,通常每次重新分配時,大小都為原來的k倍。k值越大,則重新分配內存的次數越少,但浪費的空間越多。本節通過一系列的實驗觀察列表和array的內存分配模式。
list存儲結構
list聲明后結構大體分為3部分,變量名稱--list對象(結構性數據+指針數組)--list內容,其中id表示的是list對象的位置,
v引用變量名稱,v[:]引用list對象,此規則對python其他序列結構也成立,以下示范可用id佐證,
a=b時,a和b指向同一個list對象
a=b[:]時,a的list對象和b的list對象指向同一個list內容
除此之外[0]和[:1]也是不同的:
In [30]: a[0] Out[30]: 1 In [31]: a[:1] Out[31]: [1]
空list占用空間
In [32]: sys.getsizeof([]) Out[32]: 64
一、通過getsizeof()計算列表的增長模式
step1
sys.getsizeof()可以獲得列表所占用的內存大小。請編寫程序計算一個長度為10000的列表,它的每個下標都保存列表增長到此下標時的大小:
import sys # 【你的程序】計算size列表,它的每個小標都保存增長到此下標時size列表的大小 size = [] for i in range(10000): size.append(sys.getsizeof(size)) import pylab as pl pl.plot(size, lw=2, c='b') pl.show()
圖中每個階梯跳變的位置都表示一次內存分配,而每個階梯的長度表示內存分配多出來的大小。
step2
請編寫程序計算表示每次分配內存時列表的內存大小的resize_pos數組:
import numpy as np #【你的程序】計算resize_pos,它的每個元素是size中每次分配內存的位置 # 可以使用NumPy的diff()、where()、nonzero()快速完成此計算。 size = [] for i in range(10000): size.append(sys.getsizeof(size)) size = np.array(size) new_size = np.diff(size) resize_pos = size[np.where(new_size)] # resize_pos = size[np.nonzero(new_size)] pl.plot(resize_pos, lw=2) pl.show() print ("list increase rate:") tmp = resize_pos[25:].astype(np.float) # ❶ print (np.average(tmp[1:]/tmp[:-1])) # ❷
由圖可知曲線呈指數增長,第45次分配內存時,列表的大小已經接近10000。
❷為了計算增長率,只需要計算resize_pos數組前后兩個值的商的平均值即可。
❶為了提高精度,我們只計算后半部分的平均值,注意需要用astype()方法將整數數組轉換為浮點數數組。程序的輸出如下:
list increase rate:
1.12754776209
【注】np.where索引定位的兩種用法,np.nonzero非零值bool判斷的用法,np.diff差分函數的用法。
step3
我們可以用scipy.optimize.curve_fit()對resize_pos數組進行擬合,擬合函數為指數函數:
請編寫程序用上面的公式對resize_pos數組進行擬合:
from scipy.optimize import curve_fit #【你的程序】用指數函數對resize_pos數組進行擬合 def func(x, a, b, c, d): return a * np.exp(b * x + c) + d xdata = range(len(resize_pos)) ydata = resize_pos popt, pcov = curve_fit(func, xdata, ydata) y = [func(i, *popt) for i in xdata] pl.plot(xdata, y, lw=1, c='r') pl.plot(xdata, ydata, lw=1, c='b') pl.show() print ("list increase rate by curve_fit:") print (10**popt[1])
list increase rate by curve_fit: 1.31158606108
【注意】本程序中對於scipy中的指數擬合做了示范。
Q1:元素存儲地址是否連續
首先見得的測試一下list對象存儲的內容(結構3)的內存地址,
In [1]: a=[1,2,3,'a','b','c','de',[4,5]] In [2]: id(a) Out[2]: 139717112576840 In [3]: for i in a: ...: print(id(i)) ...: 139717238769920 139717238769952 139717238769984 139717239834192 139717240077480 139717240523888 139717195281104 139717112078024 In [4]: for i in a[6]: ...: print(id(i)) ...: 139717240220952 139717240202048 In [5]: for i in a[7]: ...: print(id(i)) ...: 139717238770016 139717238770048
然后看一下相對地址,
In [6]: for i in a: ...: print(id(i)-139717238769920) ...: 0 32 64 1064272 1307560 1753968 -43488816 -126691896 In [7]: for i in a[6]: ...: print(id(i)-139717238769920) ...: 1451032 1432128 In [8]: for i in a[7]: ...: print(id(i)-139717238769920) ...: 96 128
可見,對於list對象,其元素內容並不一定線性存儲,但是由於內存分配的問題,會出現線性存儲的假象,當元素出現容器或者相對前一個元素類型改變時,內存空間就會不再連續。
Q2:list對象地址和元素地址是否連續
其實Q1已經回答了這個問題,畢竟元素地址本身就不連續,不過我們還是測試了一下,
In [22]: id(a[0])-id(a) Out[22]: 126193080
相差甚遠,而且我們分析源碼可知,list對象主體是一個指針數組,也就是id(a)所指的位置主體是一個指向元素位置的指針數組,當然還有輔助的對象頭信息之類的(python中幾個常見的“黑盒子”之 列表list)。
Q3:list對象(不含元素)占用內存情況分析
In [16]: sys.getsizeof([1,2,3,'a','b','c','de']) Out[16]: 120 In [17]: sys.getsizeof([1,2,3,'a','b','c']) Out[17]: 112 In [18]: sys.getsizeof([1,2,3,'a','b']) Out[18]: 104
可見,list每一個對象占用8字節32位空間,我們來看切片,
In [20]: sys.getsizeof(a[:3]) Out[20]: 88 In [21]: sys.getsizeof(a[:4]) Out[21]: 96 In [23]: sys.getsizeof(a[3:4]) Out[23]: 72 In [24]: sys.getsizeof(a[3:5]) Out[24]: 80
切片對象也是每個元素占8字節,但是切片也是list對象,即使從中間切(不切頭),也會包含頭信息的存儲占用。
二、通過運算時間估算array內存分配情況
遺憾的是,無論array對象的長度是多少,sys.getsizeof()的結果都不變。因此無法用上節的方法計算array對象的增長因子。
由於內存分配時會耗費比較長的時間,因此可以通過測量每次增加元素的時間,找到內存分配時的長度。請編寫測量增加元素的時間的程序:
from array import array import time #【你的程序】計算往array中添加元素的時間times times = [] times_step = [] arrays = [array('l') for i in range(1000)] start = time.time() for i in range(1000): start_step = time.time() [a.append(i) for a in arrays] end = time.time() times_step.append(end-start_step) times.append(end-start) pl.figure() pl.plot(times) pl.figure() pl.plot(times_step) pl.show()
輸出兩幅圖,前面的表示元素個數對應的程序總耗時,后面的表示每一次添加元素這一過程的耗時,注意,這張圖只有在array數量較大時才是這個形狀,數組數量不夠時折線圖差異很大。
進一步的,我們分析一下耗時顯著大於附近點(極大值)的時刻的序列對應此時元素數量的折線圖。
ts = np.array(times_step) le = range(np.sum(ts>0.00025)) si = np.squeeze(np.where(ts>0.00025)) pl.plot(le,si,lw=2) pl.show()
MXNet對臨時數組內存的優化
以MXNet中數組為例,講解一下序列計算時的內存變化以及優化方式,