理解numpy中ndarray的記憶體佈局和設計哲學
目錄
- ndarray是什麼
- ndarray的設計哲學
- ndarray的記憶體佈局
- 為什麼可以這樣設計
- 小結
- 參考
部落格:部落格園 | CSDN | blog
本文的主要目的在於理解numpy.ndarray
的記憶體結構及其背後的設計哲學。
ndarray是什麼
NumPy provides an N-dimensional array type, the ndarray, which describes a collection of “items” of the same type. The items can be indexed using for example N integers.
—— from https://docs.scipy.org/doc/numpy-1.17.0/reference/arrays.html
ndarray是numpy中的多維陣列,陣列中的元素具有相同的型別,且可以被索引。
如下所示:
>>> import numpy as np >>> a = np.array([[0,1,2,3],[4,5,6,7],[8,9,10,11]]) >>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) >>> type(a) <class 'numpy.ndarray'> >>> a.dtype dtype('int32') >>> a[1,2] 6 >>> a[:,1:3] array([[ 1, 2], [ 5, 6], [ 9, 10]]) >>> a.ndim 2 >>> a.shape (3, 4) >>> a.strides (16, 4)
注:np.array
並不是類,而是用於建立np.ndarray
物件的其中一個函式,numpy中多維陣列的類為np.ndarray
。
ndarray的設計哲學
ndarray的設計哲學在於資料儲存與其解釋方式的分離,或者說copy
和view
的分離,讓儘可能多的操作發生在解釋方式上(view
上),而儘量少地操作實際儲存資料的記憶體區域。
如下所示,像reshape
操作返回的新物件b
,a
和b
的shape
不同,但是兩者共享同一個資料block,c=b.T
,c
是b
的轉置,但兩者仍共享同一個資料block,資料並沒有發生變化,發生變化的只是資料的解釋方式。
>>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) >>> b = a.reshape(4, 3) >>> b array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]]) # reshape操作產生的是view檢視,只是對資料的解釋方式發生變化,資料實體地址相同 >>> a.ctypes.data 80831392 >>> b.ctypes.data 80831392 >>> id(a) == id(b) false # 資料在記憶體中連續儲存 >>> from ctypes import string_at >>> string_at(b.ctypes.data, b.nbytes).hex() '000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000' # b的轉置c,c仍共享相同的資料block,只改變了資料的解釋方式,“以列優先的方式解釋行優先的儲存” >>> c = b.T >>> c array([[ 0, 3, 6, 9], [ 1, 4, 7, 10], [ 2, 4, 8, 11]]) >>> c.ctypes.data 80831392 >>> string_at(c.ctypes.data, c.nbytes).hex() '000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000' >>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) # copy會複製一份新的資料,其實體地址位於不同的區域 >>> c = b.copy() >>> c array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]]) >>> c.ctypes.data 80831456 >>> string_at(c.ctypes.data, c.nbytes).hex() '000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000' # slice操作產生的也是view檢視,仍指向原來資料block中的實體地址 >>> d = b[1:3, :] >>> d array([[3, 4, 5], [6, 7, 8]]) >>> d.ctypes.data 80831404 >>> print('data buff address from {0} to {1}'.format(b.ctypes.data, b.ctypes.data + b.nbytes)) data buff address from 80831392 to 80831440
副本是一個數據的完整的拷貝,如果我們對副本進行修改,它不會影響到原始資料,實體記憶體不在同一位置。
檢視是資料的一個別稱或引用,通過該別稱或引用亦便可訪問、操作原有資料,但原有資料不會產生拷貝。如果我們對檢視進行修改,它會影響到原始資料,實體記憶體在同一位置。
檢視一般發生在:
- 1、numpy 的切片操作返回原資料的檢視。
- 2、呼叫 ndarray 的 view() 函式產生一個檢視。
副本一般發生在:
- Python 序列的切片操作,呼叫deepCopy()函式。
- 呼叫 ndarray 的 copy() 函式產生一個副本。
—— from NumPy 副本和檢視
view
機制的好處顯而易見,省記憶體,同時速度快。
ndarray的記憶體佈局
NumPy arrays consist of two major components, the raw array data (from now on, referred to as the data buffer), and the information about the raw array data. The data buffer is typically what people think of as arrays in C or Fortran, a contiguous (and fixed) block of memory containing fixed sized data items. NumPy also contains a significant set of data that describes how to interpret the data in the data buffer.
—— from NumPy internals
ndarray的記憶體佈局示意圖如下:
可大致劃分成2部分——對應設計哲學中的資料部分和解釋方式:
- raw array data:為一個連續的memory block,儲存著原始資料,類似C或Fortran中的陣列,連續儲存
- metadata:是對上面記憶體塊的解釋方式
metadata都包含哪些資訊呢?
dtype
:資料型別,指示了每個資料佔用多少個位元組,這幾個位元組怎麼解釋,比如int32
、float32
等;ndim
:有多少維;shape
:每維上的數量;strides
:維間距,即到達當前維下一個相鄰資料需要前進的位元組數,因考慮記憶體對齊,不一定為每個資料佔用位元組數的整數倍;
上面4個資訊構成了ndarray
的indexing schema,即如何索引到指定位置的資料,以及這個資料該怎麼解釋。
除此之外的資訊還有:位元組序(大端小端)、讀寫許可權、C-order(行優先儲存) or Fortran-order(列優先儲存)等,如下所示,
>>> a.flags
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
WRITEBACKIFCOPY : False
UPDATEIFCOPY : False
ndarray
的底層是C和Fortran實現,上面的屬性可以在其原始碼中找到對應,具體可見PyArrayObject和PyArray_Descr等結構體。
為什麼可以這樣設計
為什麼ndarray
可以這樣設計?
因為ndarray
是為矩陣運算服務的,ndarray
中的所有資料都是同一種類型,比如int32
、float64
等,每個資料佔用的位元組數相同、解釋方式也相同,所以可以稠密地排列在一起,在取出時根據dtype
現copy一份資料組裝成scalar
物件輸出。這樣極大地節省了空間,scalar
物件中除了資料之外的域沒必要重複儲存,同時因為連續記憶體的原因,可以按秩訪問,速度也要快得多。
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[1,1]
5
>>> i,j = a[1,1], a[1,1]
# i和j為不同的物件,訪問一次就“組裝一個”物件
>>> id(i)
102575536
>>> id(j)
102575584
>>> a[1,1] = 4
>>> i
5
>>> j
5
>>> a
array([[ 0, 1, 2, 3],
[ 4, 4, 6, 7],
[ 8, 9, 10, 11]])
# isinstance(val, np.generic) will return True if val is an array scalar object. Alternatively, what kind of array scalar is present can be determined using other members of the data type hierarchy.
>> isinstance(i, np.generic)
True
這裡,可以將ndarray
與python中的list
對比一下,list
可以容納不同型別的物件,像string
、int
、tuple
等都可以放在一個list
裡,所以list
中存放的是物件的引用,再通過引用找到具體的物件,這些物件所在的實體地址並不是連續的,如下所示
所以相對ndarray
,list
訪問到資料需要多跳轉1次,list
只能做到對物件引用的按秩訪問,對具體的資料並不是按秩訪問,所以效率上ndarray
比list
要快得多,空間上,因為ndarray
只把資料緊密儲存,而list
需要把每個物件的所有域值都存下來,所以ndarray
比list
要更省空間。
小結
下面小結一下:
ndarray
的設計哲學在於資料與其解釋方式的分離,讓絕大部分多維陣列操作只發生在解釋方式上;ndarray
中的資料在實體記憶體上連續儲存,在讀取時根據dtype
現組裝成物件輸出,可以按秩訪問,效率高省空間;- 之所以能這樣實現,在於
ndarray
是為矩陣運算服務的,所有資料單元都是同種型別。
參考
- Array objects
- NumPy internals
- NumPy C Code Explanations
- Python Types and C-Structures
- How is the memory allocated for numpy arrays in python?
- NumPy 副本和檢視