连续数组和非连续数组的区别是什么?

傻瓜手册中关于重塑()函数,它说

>>> 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 语言中的连续内存块,如 什么是连续内存块?
  2. 这两者之间有什么性能差异吗? 我们什么时候应该使用其中一种?
  3. 为什么转置使数组不连续?
  4. 为什么 c.shape = (20)抛出一个错误 incompatible shape for a non-contiguous array

谢谢你的回答!

98034 次浏览

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

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

enter image description here

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

enter image description here

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

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

enter image description here


就性能而言,访问相邻的内存地址通常比访问更“分散”的地址要快(从 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的行放在一起:

enter image description here

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

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

如果我们改为编写 arr2.reshape(12),NumPy 将把 arr2的值复制到一个新的内存块中(因为它不能返回对这个形状的原始数据的视图)。

也许这个包含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])

你可以得到两者的1D 视图

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...顺序,不能以1d 数组中的 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也许会有所帮助。步长是它需要走多远(以字节为单位)才能到达下一个值。对于2d 数组,有两个步长值:

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)

换位只是切换步长的顺序。下一行只有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,...,没有一个1d 的步长可以给出 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命令中。