【问题标题】:What is the difference between contiguous and non-contiguous arrays?连续数组和非连续数组有什么区别?
【发布时间】:2015-01-15 20:55:45
【问题描述】:

在关于 reshape() 函数的numpy manual 中,它说

>>> a = np.zeros((10, 2))
# A transpose make the array non-contiguous
>>> b = a.T
# Taking a view makes it possible to modify the shape without modifying the
# initial object.
>>> c = b.view()
>>> c.shape = (20)
AttributeError: incompatible shape for a non-contiguous array

我的问题是:

  1. 什么是连续数组和非连续数组?是不是类似于C语言中的连续内存块What is a contiguous memory block?
  2. 这两者之间有性能差异吗?我们什么时候应该使用其中一种?
  3. 为什么转置会使数组不连续?
  4. 为什么c.shape = (20)会抛出错误incompatible shape for a non-contiguous array

感谢您的回答!

【问题讨论】:

    标签: python arrays numpy memory


    【解决方案1】:

    连续数组只是存储在完整内存块中的数组:要访问数组中的下一个值,我们只需移动到下一个内存地址即可。

    考虑二维数组arr = np.arange(12).reshape(3,4)。它看起来像这样:

    在计算机的内存中,arr 的值是这样存储的:

    这意味着arr 是一个C 连续 数组,因为 存储为连续的内存块。下一个内存地址保存该行的下一行值。如果我们想向下移动一列,我们只需要跳过三个块(例如,从 0 跳转到 4 意味着我们跳过了 1,2 和 3)。

    arr.T 转置数组意味着C 连续性丢失,因为相邻的行条目不再位于相邻的内存地址中。但是,arr.TFortran 连续的,因为 位于连续的内存块中:


    在性能方面,访问彼此相邻的内存地址通常比访问更“分散”的地址更快(从 RAM 中获取值可能需要为 CPU 获取和缓存许多相邻地址.) 这意味着对连续数组的操作通常会更快。

    作为 C 连续内存布局的结果,按行操作通常比按列操作更快。例如,您通常会发现

    np.sum(arr, axis=1) # sum the rows
    

    略快于:

    np.sum(arr, axis=0) # sum the columns
    

    同样,对于 Fortran 连续数组,对列的操作会稍微快一些。


    最后,为什么我们不能通过分配一个新的形状来展平 Fortran 连续数组?

    >>> arr2 = arr.T
    >>> arr2.shape = 12
    AttributeError: incompatible shape for a non-contiguous array
    

    为了让这成为可能,NumPy 必须像这样将arr.T 的行放在一起:

    (设置 shape 属性直接假定 C 顺序 - 即 NumPy 尝试按行执行操作。)

    这是不可能的。对于任何轴,NumPy 都需要有一个常数 步长(要移动的字节数)才能到达数组的下一个元素。以这种方式展平arr.T 需要在内存中前后跳过以检索数组的连续值。

    如果我们改为写arr2.reshape(12),NumPy 会将 arr2 的值复制到新的内存块中(因为它无法返回查看该形状的原始数据的视图)。

    【讨论】:

    • 我很难理解这一点,您能详细说明一下吗?在我认为内存中不可能排序的最新图形表示中,实际上步幅是恒定的。例如,从 0 到 1 的步幅是 1 个字节(假设每个元素都是一个字节)并且每列都是相同的。同样,从行中的一个元素到下一个元素的步幅为 4 个字节,并且它也是恒定的。
    • @Vesnog 将 2D arr2 重塑为 1D 形状 (12,) 的失败使用 C 顺序,这意味着轴 1 在轴 0 之前展开(即四行中的每一行都需要彼此相邻放置以创建所需的一维阵列)。使用恒定步长(要跳转访问的字节)从缓冲区中读取这个整数序列(0、4、8、1、5、9、2、6、10、3、7、11)是不可能的这些元素依次为 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4)。 NumPy 要求每个轴有一个恒定的步长。
    • 谢谢 一开始我以为它会创建一个新数组,但它使用了旧数组的内存。
    • @AlexRiley 当一个数组被标记为更接近 C 或 F 排序时,幕后会发生什么?例如,获取每个 NxD 数组 arr 和 print(arr[:,::-1].flags)。在这种情况下会发生什么?我猜这个数组确实是 C 或 F 有序的,但是其中哪一个呢?如果两个标志都是 False,我们会失去哪些 numpy 优化?
    • @Jjang:NumPy 认为数组是 C 顺序还是 F 顺序完全取决于形状和步幅(标准是 here)。因此,虽然 arr[:, ::-1] 是与 arr 相同的内存缓冲区的视图,但 NumPy 不认为它是 C 或 F 顺序,因为它以“非标准”顺序遍历缓冲区中的值......
    【解决方案2】:

    也许这个包含 12 个不同数组值的示例会有所帮助:

    In [207]: x=np.arange(12).reshape(3,4).copy()
    
    In [208]: x.flags
    Out[208]: 
      C_CONTIGUOUS : True
      F_CONTIGUOUS : False
      OWNDATA : True
      ...
    In [209]: x.T.flags
    Out[209]: 
      C_CONTIGUOUS : False
      F_CONTIGUOUS : True
      OWNDATA : False
      ...
    

    C order 值按照它们生成的顺序排列。转置的不是

    In [212]: x.reshape(12,)   # same as x.ravel()
    Out[212]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
    
    In [213]: x.T.reshape(12,)
    Out[213]: array([ 0,  4,  8,  1,  5,  9,  2,  6, 10,  3,  7, 11])
    

    您可以获得两者的一维视图

    In [214]: x1=x.T
    
    In [217]: x.shape=(12,)
    

    x的形状也可以改变。

    In [220]: x1.shape=(12,)
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-220-cf2b1a308253> in <module>()
    ----> 1 x1.shape=(12,)
    
    AttributeError: incompatible shape for a non-contiguous array
    

    但是转置的形状不能改变。 data 仍处于0,1,2,3,4... 顺序中,无法在一维数组中作为0,4,8... 访问。

    x1 的副本可以更改:

    In [227]: x2=x1.copy()
    
    In [228]: x2.flags
    Out[228]: 
      C_CONTIGUOUS : True
      F_CONTIGUOUS : False
      OWNDATA : True
      ...
    In [229]: x2.shape=(12,)
    

    查看strides 也可能有所帮助。步幅是它必须走多远(以字节为单位)才能到达下一个值。对于二维数组,将有 2 个步幅值:

    In [233]: x=np.arange(12).reshape(3,4).copy()
    
    In [234]: x.strides
    Out[234]: (16, 4)
    

    要到达下一行,步长 16 个字节,下一列只有 4 个。

    In [235]: x1.strides
    Out[235]: (4, 16)
    

    Transpose 只是切换步幅的顺序。下一行只有 4 个字节——即下一个数字。

    In [236]: x.shape=(12,)
    
    In [237]: x.strides
    Out[237]: (4,)
    

    改变形状也会改变步幅 - 一次只需 4 个字节的缓冲区。

    In [238]: x2=x1.copy()
    
    In [239]: x2.strides
    Out[239]: (12, 4)
    

    尽管x2 看起来就像x1,但它有自己的数据缓冲区,并且值的顺序不同。下一列现在超过 4 个字节,而下一行是 12 (3*4)。

    In [240]: x2.shape=(12,)
    
    In [241]: x2.strides
    Out[241]: (4,)
    

    x 一样,将形状更改为1d 会将步幅减少到(4,)

    对于x1,使用0,1,2,... 顺序中的数据,没有一维步幅可以提供0,4,8...

    __array_interface__ 是另一种显示数组信息的有用方式:

    In [242]: x1.__array_interface__
    Out[242]: 
    {'strides': (4, 16),
     'typestr': '<i4',
     'shape': (4, 3),
     'version': 3,
     'data': (163336056, False),
     'descr': [('', '<i4')]}
    

    x1 数据缓冲区地址将与 x 相同,并与之共享数据。 x2 有不同的缓冲区地址。

    您还可以尝试将order='F' 参数添加到copyreshape 命令。

    【讨论】:

      猜你喜欢
      • 2017-01-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-09
      • 1970-01-01
      • 2017-09-14
      • 2016-01-31
      • 1970-01-01
      相关资源
      最近更新 更多