web-dev-qa-db-fra.com

Quelle est la différence entre des tableaux contigus et non contigus?

Dans le numpy manual à propos de la fonction reshape (), il est dit

>>> 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

Mes questions sont:

  1. Que sont des tableaux continus et non contigus? Est-il similaire au bloc de mémoire contigu en C comme Qu'est-ce qu'un bloc de mémoire contigu?
  2. Y at-il une différence de performance entre ces deux? Quand devrions-nous utiliser l'un ou l'autre?
  3. Pourquoi transpose-t-il le tableau non contigu?
  4. Pourquoi c.shape = (20) génère-t-il une erreur incompatible shape for a non-contiguous array?

Merci pour votre réponse!

66
jdeng

Un tableau contigu est juste un tableau stocké dans un bloc de mémoire ininterrompu: pour accéder à la valeur suivante du tableau, nous passons simplement à l'adresse de mémoire suivante.

Considérons le tableau 2D arr = np.arange(12).reshape(3,4). Cela ressemble à ceci:

enter image description here

Dans la mémoire de l'ordinateur, les valeurs de arr sont stockées comme ceci:

enter image description here

Cela signifie que arr est un tableau C contigu car les lignes sont stockés sous forme de blocs de mémoire contigus. L'adresse mémoire suivante contient la valeur de la ligne suivante sur cette ligne. Si nous voulons descendre d'une colonne, il nous suffit de sauter trois blocs (par exemple, passer de 0 à 4 signifie que nous sautons 1,2 et 3).

Transposer le tableau avec arr.T Signifie que la contiguïté C est perdue car les entrées de lignes adjacentes ne figurent plus dans des adresses mémoire adjacentes. Cependant, arr.T Est Fortran contigu puisque les colonnes sont dans des blocs de mémoire contigus:

enter image description here


En termes de performances, accéder à des adresses de mémoire adjacentes est très souvent plus rapide que d’accéder à des adresses plus "dispersées" (extraire une valeur de RAM pourrait entraîner la création d’un certain nombre d’adresses voisines). récupéré et mis en cache pour la CPU.) Cela signifie que les opérations sur des tableaux contigus seront souvent plus rapides.

En conséquence de la disposition de la mémoire C contiguë, les opérations ligne par ligne sont généralement plus rapides que les opérations colonne. Par exemple, vous constaterez généralement que

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

est légèrement plus rapide que:

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

De même, les opérations sur les colonnes seront légèrement plus rapides pour les tableaux Fortran contigus.


Enfin, pourquoi ne pouvons-nous pas aplatir le tableau contigu de Fortran en attribuant une nouvelle forme?

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

Pour que cela soit possible, NumPy devrait assembler les lignes de arr.T Comme suit:

enter image description here

(La définition de l’attribut shape suppose directement l’ordre C: c’est-à-dire que NumPy tente d’effectuer l’opération par rangée.)

C'est impossible à faire. Pour tout axe, NumPy doit avoir une constante longueur de foulée (le nombre d'octets à déplacer) pour accéder à l'élément suivant du tableau. Pour aplatir arr.T De cette manière, il faudrait sauter en avant et en arrière en mémoire pour récupérer des valeurs consécutives du tableau.

Si nous écrivions plutôt arr2.reshape(12), NumPy copierait les valeurs d'arr2 dans un nouveau bloc de mémoire (car il ne peut pas restituer les données d'origine pour cette forme).

141
Alex Riley

Peut-être que cet exemple avec 12 valeurs de tableau différentes aidera:

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
  ...

Le C order valeurs sont dans l’ordre dans lequel elles ont été générées. Les valeurs transposées ne sont pas

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])

Vous pouvez obtenir une vue 1d des deux

In [214]: x1=x.T

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

la forme de x peut également être modifiée.

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

Mais la forme de la transposition ne peut pas être modifiée. Le data est toujours dans le 0,1,2,3,4... ordre, auquel on ne peut accéder par 0,4,8... dans un tableau 1d.

Mais une copie de x1 peut être changé:

In [227]: x2=x1.copy()

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

Regarder strides pourrait aussi aider. Un pas est la distance (en octets) qu'il doit parcourir pour passer à la valeur suivante. Pour un tableau 2D, il y aura 2 valeurs de foulée:

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

In [234]: x.strides
Out[234]: (16, 4)

Pour accéder à la ligne suivante, étape 16 octets, colonne suivante uniquement 4.

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

Transposer change simplement l'ordre des progrès. La rangée suivante n’est que de 4 octets, c’est-à-dire le nombre suivant.

In [236]: x.shape=(12,)

In [237]: x.strides
Out[237]: (4,)

Changer la forme modifie également les foulées - parcourez simplement la mémoire tampon 4 octets à la fois.

In [238]: x2=x1.copy()

In [239]: x2.strides
Out[239]: (12, 4)

Même si x2 ressemble à x1, il a son propre tampon de données, avec les valeurs dans un ordre différent. La colonne suivante a maintenant 4 octets de plus, tandis que la rangée suivante est 12 (3 * 4).

In [240]: x2.shape=(12,)

In [241]: x2.strides
Out[241]: (4,)

Et comme avec x, changer la forme en 1d réduit les pas à (4,).

Pour x1, avec les données dans le 0,1,2,... ordre, il n'y a pas une foulée 1d qui donnerait 0,4,8....

__array_interface__ est un autre moyen utile d’afficher des informations sur un tableau:

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

Le x1 l'adresse du tampon de données sera la même que pour x, avec lequel il partage les données. x2 a une adresse de tampon différente.

Vous pouvez également expérimenter en ajoutant un order='F' paramètre sur les commandes copy et reshape.

7
hpaulj