zoukankan      html  css  js  c++  java
  • 理解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是为矩阵运算服务的,所有数据单元都是同种类型

    参考

  • 相关阅读:
    .NET开发不可不知、不可不用的辅助类(一)
    .NET开发不可不知、不可不用的辅助类(三)(报表导出终结版)
    如何获取类或属性的自定义特性(Attribute)
    如何使用C#进行Visio二次开发
    列表查询组件代码, 简化拼接条件SQL语句的麻烦
    强大的模板引擎开源软件NVelocity
    自己编写的操作实体类的分页控件, 实现页码层与数据库的具体的信息隔离
    代码生成工具随笔(1) 关于代码生成器
    老歌新唱使用VB6开发的ActiveX实现.NET程序的混淆加密
    VB6中如何使用C#开发的WebService进行开发
  • 原文地址:https://www.cnblogs.com/shine-lee/p/12293097.html
Copyright © 2011-2022 走看看