理解numpy中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的設計哲學在於數據存儲與其解釋方式的分離,或者說copyview的分離,讓儘可能多的操作發生在解釋方式上(view上),而儘量少地操作實際存儲數據的內存區域。

如下所示,像reshape操作返回的新對象babshape不同,但是兩者共享同一個數據block,c=b.Tcb的轉置,但兩者仍共享同一個數據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的內存佈局示意圖如下:

https://stackoverflow.com/questions/57262885/how-is-the-memory-allocated-for-numpy-arrays-in-python

可大致劃分成2部分——對應設計哲學中的數據部分和解釋方式:

  • raw array data:爲一個連續的memory block,存儲着原始數據,類似C或Fortran中的數組,連續存儲
  • metadata:是對上面內存塊的解釋方式

metadata都包含哪些信息呢?

  • dtype數據類型,指示了每個數據佔用多少個字節,這幾個字節怎麼解釋,比如int32float32等;
  • ndim:有多少維;
  • shape:每維上的數量;
  • strides維間距,即到達當前維下一個相鄰數據需要前進的字節數,因考慮內存對齊,不一定爲每個數據佔用字節數的整數倍;

上面4個信息構成了ndarrayindexing 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實現,上面的屬性可以在其源碼中找到對應,具體可見PyArrayObjectPyArray_Descr等結構體。

爲什麼可以這樣設計

爲什麼ndarray可以這樣設計?

因爲ndarray是爲矩陣運算服務的,ndarray中的所有數據都是同一種類型,比如int32float64等,每個數據佔用的字節數相同、解釋方式也相同,所以可以稠密地排列在一起,在取出時根據dtype現copy一份數據組裝成scalar對象輸出。這樣極大地節省了空間,scalar對象中除了數據之外的域沒必要重複存儲,同時因爲連續內存的原因,可以按秩訪問,速度也要快得多。

https://docs.scipy.org/doc/numpy-1.17.0/reference/arrays.html

>>> 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可以容納不同類型的對象,像stringinttuple等都可以放在一個list裏,所以list中存放的是對象的引用,再通過引用找到具體的對象,這些對象所在的物理地址並不是連續的,如下所示

https://jakevdp.github.io/PythonDataScienceHandbook/02.01-understanding-data-types.html

所以相對ndarraylist訪問到數據需要多跳轉1次,list只能做到對對象引用的按秩訪問,對具體的數據並不是按秩訪問,所以效率上ndarraylist要快得多,空間上,因爲ndarray只把數據緊密存儲,而list需要把每個對象的所有域值都存下來,所以ndarraylist要更省空間。

小結

下面小結一下:

  • ndarray的設計哲學在於數據與其解釋方式的分離,讓絕大部分多維數組操作只發生在解釋方式上
  • ndarray中的數據在物理內存上連續存儲,在讀取時根據dtype現組裝成對象輸出,可以按秩訪問,效率高省空間
  • 之所以能這樣實現,在於ndarray是爲矩陣運算服務的,所有數據單元都是同種類型

參考

發佈了55 篇原創文章 · 獲贊 77 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章