『Python』內存分析_list和array

零、預備知識

在Python中,列表是一個動態的指針數組,而array模塊所提供的array對象則是保存相同類型的數值的動態數組。因爲array直接保存值,所以它所使用的內存比列表少。列表和array都是動態數組,所以往其中添加新元素,而沒有空間保存新的元素時,它們會自動從新分配內存塊,並將原來的內存中的值複製到新的內存塊中。爲了減小從新分配內存的次數,一般每次從新分配時,大小都爲原來的k倍。k值越大,則從新分配內存的次數越少,但浪費的空間越多。本節經過一系列的實驗觀察列表和array的內存分配模式。html

list存儲結構

list聲明後結構大致分爲3部分,變量名稱--list對象(結構性數據+指針數組)--list內容,其中id表示的是list對象的位置,python

v引用變量名稱,v[:]引用list對象,此規則對python其餘序列結構也成立,如下示範可用id佐證,數組

a=b時,a和b指向同一個list對象app

a=b[:]時,a的list對象和b的list對象指向同一個list內容函數

除此以外[0]和[:1]也是不一樣的:post

In [30]: a[0]
Out[30]: 1

In [31]: a[:1]
Out[31]: [1]

 空list佔用空間測試

In [32]: sys.getsizeof([])
Out[32]: 64

1、經過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()

圖中每一個階梯跳變的位置都表示一次內存分配,而每一個階梯的長度表示內存分配多出來的大小。 url

 

step2

請編寫程序計算表示每次分配內存時列表的內存大小的resize_pos數組:spa

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數組進行擬合,擬合函數爲指數函數:

a 10^{b x + c} + d

請編寫程序用上面的公式對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對象,即便從中間切(不切頭),也會包含頭信息的存儲佔用。

 2、經過運算時間估算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中數組爲例,講解一下序列計算時的內存變化以及優化方式,

相關文章
相關標籤/搜索