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

101

Dans le manuel numpy sur 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 les 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 a-t-il une différence de performance entre ces deux? Quand devons-nous utiliser l'un ou l'autre?
  3. Pourquoi transpose rend le tableau non contigu?
  4. Pourquoi c.shape = (20)jette une erreur incompatible shape for a non-contiguous array?

Merci pour votre réponse!

jdeng
la source

Réponses:

222

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 mémoire suivante.

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

entrez la description de l'image ici

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

entrez la description de l'image ici

Cela signifie qu'il arrs'agit d'un tableau contigu en C car les lignes sont stockées en tant que blocs contigus de mémoire. L'adresse mémoire suivante contient la valeur de ligne suivante sur cette ligne. Si nous voulons descendre d'une colonne, il suffit de sauter par-dessus trois blocs (par exemple, sauter de 0 à 4 signifie que nous sautons 1,2 et 3).

Transposer le tableau avec arr.Tsignifie que la contiguïté C est perdue car les entrées de ligne adjacentes ne sont plus dans des adresses mémoire adjacentes. Cependant, Fortranarr.T est-il contigu puisque les colonnes sont dans des blocs de mémoire contigus:

entrez la description de l'image ici


En termes de performances, accéder à des adresses mémoire qui sont côte à côte est très souvent plus rapide que d'accéder à des adresses qui sont plus «étalées» (la récupération d'une valeur dans la RAM peut entraîner la récupération et la mise en cache d'un certain nombre d'adresses voisines pour le CPU.) signifie que les opérations sur des tableaux contigus seront souvent plus rapides.

En raison de la disposition de la mémoire contiguë C, les opérations par ligne sont généralement plus rapides que les opérations par 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 contigus Fortran.


Enfin, pourquoi ne pouvons-nous pas aplatir le tableau contigu 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 rassembler les lignes de arr.Tcomme ceci:

entrez la description de l'image ici

(La définition de l' shapeattribut suppose directement l'ordre C - c'est-à-dire que NumPy essaie d'effectuer l'opération par ligne.)

C'est impossible à faire. Pour tout axe, NumPy doit avoir une longueur de pas constante (le nombre d'octets à déplacer) pour accéder à l'élément suivant du tableau. L'aplatissement arr.Tde cette manière nécessiterait de sauter en avant et en arrière dans la mémoire pour récupérer des valeurs consécutives du tableau.

Si nous écrivions à la arr2.reshape(12)place, NumPy copierait les valeurs de arr2 dans un nouveau bloc de mémoire (car il ne peut pas renvoyer une vue sur les données d'origine pour cette forme).

Alex Riley
la source
J'ai de la difficulté à comprendre cela, pouvez-vous nous en dire un peu plus? Dans la dernière représentation graphique de l'ordre impossible dans la mémoire, en fait, les pas sont constants à mon avis. Par exemple, pour passer de 0 à 1, la foulée est de 1 octet (disons que chaque élément est un octet) et c'est la même chose pour chaque colonne. De même, la foulée est de 4 octets pour passer d'un élément de la ligne au suivant et elle est également constante.
Vesnog le
2
@Vesnog le remodelage échoué de la 2D arr2en forme 1D (12,)utilise l'ordre C, ce qui signifie que cet axe 1 est déroulé avant l'axe 0 (c'est-à-dire que chacune des quatre lignes doit être placée l'une à côté de l'autre pour créer le tableau 1D souhaité). Il est impossible de lire cette séquence d'entiers (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) hors du tampon en utilisant une longueur de foulée constante (les octets à sauter pour visiter ces éléments dans l'ordre seraient 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy nécessite une longueur de foulée continue par axe.
Alex Riley le
Merci au début, j'ai pensé que cela créerait un nouveau tableau, mais il utilise la mémoire de l'ancien.
Vesnog
@AlexRiley Que se passe-t-il dans les coulisses lorsqu'un tableau est marqué plus proche C ou F ordonné? Par exemple, prenez chaque tableau NxD arr et print (arr [:, :: - 1] .flags). Que se passe-t-il dans cette situation? Je suppose que le tableau est en effet ordonné C ou F, mais lequel de ceux-ci? Et quelles optimisations de numpy perdons-nous si les deux indicateurs sont faux?
Jjang
@Jjang: si NumPy considère que le tableau est C ou F, l'ordre dépend entièrement de la forme et des foulées (les critères sont ici ). Donc, alors que arr[:, ::-1]est une vue du même tampon mémoire que arr, NumPy ne le considère pas dans l'ordre C ou F car il a traversé les valeurs dans le tampon dans un ordre "non standard" ...
Alex Riley
12

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

Les C ordervaleurs 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 des vues 1d des deux

In [214]: x1=x.T

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

la forme de xpeut é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 dataest toujours dans l' 0,1,2,3,4...ordre, qui n'est pas accessible comme 0,4,8...dans un tableau 1d.

Mais une copie de x1peut être modifiée:

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 stridespeut également aider. Un pas est la distance (en octets) qu'il doit parcourir pour atteindre 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 foulées. La ligne suivante ne fait que 4 octets, c'est-à-dire le numéro suivant.

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

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

Changer la forme change également les foulées - il suffit de parcourir le tampon 4 octets à la fois.

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

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

Même s'il x2ressemble x1, il a son propre tampon de données, avec les valeurs dans un ordre différent. La colonne suivante a maintenant 4 octets au-dessus, tandis que la ligne 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 foulées à (4,).

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

__array_interface__ est un autre moyen utile d'afficher les informations du tableau:

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

L' x1adresse du tampon de données sera la même que pourx , avec laquelle il partage les données. x2a une adresse de tampon différente.

Vous pouvez également essayer d'ajouter un order='F'paramètre aux commandes copyet reshape.

hpaulj
la source