Was ist der Unterschied zwischen zusammenhängenden und nicht zusammenhängenden Arrays?

98

Im numpy-Handbuch zur Funktion reshape () heißt es

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

Meine Fragen sind:

  1. Was sind kontinuierliche und nicht zusammenhängende Arrays? Ist es dem zusammenhängenden Speicherblock in C ähnlich wie Was ist ein zusammenhängender Speicherblock?
  2. Gibt es einen Leistungsunterschied zwischen diesen beiden? Wann sollten wir den einen oder anderen verwenden?
  3. Warum macht die Transponierung das Array nicht zusammenhängend?
  4. Warum c.shape = (20)wirft ein Fehler incompatible shape for a non-contiguous array?

Danke für deine Antwort!

jdeng
quelle

Antworten:

218

Ein zusammenhängendes Array ist nur ein Array, das in einem ununterbrochenen Speicherblock gespeichert ist: Um auf den nächsten Wert im Array zuzugreifen, wechseln wir einfach zur nächsten Speicheradresse.

Betrachten Sie das 2D-Array arr = np.arange(12).reshape(3,4). Es sieht aus wie das:

Geben Sie hier die Bildbeschreibung ein

Im Speicher des Computers werden die Werte von arrwie folgt gespeichert:

Geben Sie hier die Bildbeschreibung ein

Dies bedeutet, dass arres sich um ein C-zusammenhängendes Array handelt, da die Zeilen als zusammenhängende Speicherblöcke gespeichert sind. Die nächste Speicheradresse enthält den nächsten Zeilenwert in dieser Zeile. Wenn wir eine Spalte nach unten verschieben möchten, müssen wir nur über drei Blöcke springen (z. B. von 0 auf 4 zu springen bedeutet, dass wir 1,2 und 3 überspringen).

Das Transponieren des Arrays mit arr.Tbedeutet, dass die C-Kontiguität verloren geht, weil sich benachbarte Zeileneinträge nicht mehr in benachbarten Speicheradressen befinden. Allerdings arr.Tist Fortran zusammenhängende , da die Spalten in zusammenhängende Speicherblöcke sind:

Geben Sie hier die Bildbeschreibung ein


In Bezug auf die Leistung ist der Zugriff auf Speicheradressen, die nebeneinander liegen, sehr oft schneller als der Zugriff auf Adressen, die "verteilter" sind (das Abrufen eines Werts aus dem RAM kann dazu führen, dass mehrere benachbarte Adressen für die CPU abgerufen und zwischengespeichert werden.) bedeutet, dass Operationen über zusammenhängende Arrays oft schneller sind.

Infolge des zusammenhängenden C-Speicherlayouts sind zeilenweise Operationen normalerweise schneller als spaltenweise Operationen. Zum Beispiel werden Sie das normalerweise finden

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

ist etwas schneller als:

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

In ähnlicher Weise sind Operationen an Spalten für zusammenhängende Fortran-Arrays etwas schneller.


Warum können wir das zusammenhängende Fortran-Array nicht durch Zuweisen einer neuen Form reduzieren?

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

Damit dies möglich ist, müsste NumPy die Zeilen folgendermaßen arr.Tzusammensetzen:

Geben Sie hier die Bildbeschreibung ein

(Das Setzen des shapeAttributs setzt direkt die Reihenfolge C voraus - dh NumPy versucht, die Operation zeilenweise auszuführen.)

Das ist unmöglich zu machen. Für jede Achse muss NumPy eine konstante Schrittlänge (die Anzahl der zu verschiebenden Bytes) haben, um zum nächsten Element des Arrays zu gelangen. Das Reduzieren arr.Tauf diese Weise würde das Vor- und Zurückspringen im Speicher erfordern, um aufeinanderfolgende Werte des Arrays abzurufen.

Wenn wir arr2.reshape(12)stattdessen schreiben würden, würde NumPy die Werte von arr2 in einen neuen Speicherblock kopieren (da es keine Ansicht zu den Originaldaten für diese Form zurückgeben kann).

Alex Riley
quelle
Ich habe Schwierigkeiten, das zu verstehen. Können Sie bitte etwas näher darauf eingehen? In der neuesten grafischen Darstellung der unmöglichen Ordnung im Gedächtnis sind die Schritte meiner Meinung nach tatsächlich konstant. Um beispielsweise von 0 auf 1 zu wechseln, beträgt der Schritt 1 Byte (sagen wir, jedes Element ist ein Byte) und ist für jede Spalte gleich. Ebenso beträgt der Schritt 4 Bytes, um von einem Element in der Zeile zum nächsten zu gelangen, und er ist auch konstant.
Vesnog
2
@Vesnog Die fehlgeschlagene Umformung des 2D arr2in die 1D-Form (12,)verwendet die C-Reihenfolge, was bedeutet, dass die Achse 1 vor der Achse 0 abgewickelt wird (dh jede der vier Zeilen muss nebeneinander platziert werden, um das gewünschte 1D-Array zu erstellen). Es ist unmöglich, diese Folge von ganzen Zahlen (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) mit einer konstanten Schrittlänge (die zu besuchenden Bytes) aus dem Puffer zu lesen Diese Elemente wären nacheinander 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy erfordert eine konstante Schrittlänge pro Achse.
Alex Riley
Vielen Dank, zuerst dachte ich, dass es ein neues Array erstellen wird, aber es verwendet den Speicher des alten.
Vesnog
@AlexRiley Was passiert hinter den Kulissen, wenn ein Array als Nachbar C oder F bestellt markiert ist? Nehmen Sie zum Beispiel jedes NxD-Array arr und drucken Sie (arr [:, :: - 1] .flags). Was passiert in dieser Situation? Ich denke, das Array ist in der Tat C oder F geordnet, aber welches davon? Und welche Numpy-Optimierungen verlieren wir, wenn beide Flags falsch sind?
Jjang
@Jjang: Ob NumPy das Array als C- oder F-Reihenfolge ansieht, hängt vollständig von Form und Schritten ab (die Kriterien sind hier ). Während arr[:, ::-1]also eine Ansicht des gleichen Speicherpuffers wie arrangezeigt wird, betrachtet NumPy diese nicht als C- oder F-Reihenfolge, da die Werte im Puffer in einer "nicht standardmäßigen" Reihenfolge durchlaufen wurden ...
Alex Riley
12

Vielleicht hilft dieses Beispiel mit 12 verschiedenen Array-Werten:

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

Die C orderWerte sind in der Reihenfolge, in der sie generiert wurden. Die transponierten sind es nicht

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

Sie können 1d Ansichten von beiden erhalten

In [214]: x1=x.T

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

Die Form von xkann auch geändert werden.

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

Die Form der Transponierten kann jedoch nicht geändert werden. Das dataist immer noch in der 0,1,2,3,4...Reihenfolge, auf die nicht wie 0,4,8...in einem 1d-Array zugegriffen werden kann .

Eine Kopie von x1kann jedoch geändert werden:

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

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

Ein Blick strideskönnte auch helfen. Ein Schritt ist, wie weit (in Bytes) es gehen muss, um zum nächsten Wert zu gelangen. Für ein 2d-Array gibt es 2 Schrittwerte:

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

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

Um zur nächsten Zeile zu gelangen, Schritt 16 Bytes, nächste Spalte nur 4.

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

Transponieren ändert nur die Reihenfolge der Schritte. Die nächste Zeile besteht nur aus 4 Bytes, dh der nächsten Nummer.

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

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

Durch Ändern der Form werden auch die Schritte geändert. Gehen Sie jeweils 4 Byte durch den Puffer.

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

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

Obwohl es so x2aussieht x1, hat es einen eigenen Datenpuffer mit den Werten in einer anderen Reihenfolge. Die nächste Spalte ist jetzt 4 Byte länger, während die nächste Zeile 12 (3 * 4) ist.

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

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

Und wie bei xreduziert das Ändern der Form auf 1d die Schritte auf (4,).

Denn x1mit Daten in der 0,1,2,...Reihenfolge gibt es keinen 1d-Schritt, der geben würde 0,4,8....

__array_interface__ ist eine weitere nützliche Methode zum Anzeigen von Array-Informationen:

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

Die x1Datenpufferadresse ist dieselbe wie für x, mit der die Daten geteilt werden. x2hat eine andere Pufferadresse.

Sie können auch mit dem Hinzufügen eines order='F'Parameters zu den Befehlen copyund experimentieren reshape.

hpaulj
quelle