Die Zugriffszeit eines numply-Arrays ist viel stärker vom letzten Index betroffen als der vorletzte

8

Dies ist ein Follow-up zu dieser Antwort zu meiner vorherigen Frage schnellste Annäherung, um Tausende von Bildern in ein großes numpy Array zu lesen .

In Kapitel 2.3 "Speicherzuweisung des ndarray" schreibt Travis Oliphant Folgendes bezüglich der Indizes werden im Speicher für C-geordnete numpy Arrays zugegriffen.

  

... um sequentiell durch den Computerspeicher zu gehen, wird der letzte Index zuerst inkrementiert, gefolgt vom vorletzten Index und so weiter.

Dies kann durch ein Benchmarking der Zugriffszeit von 2-D-Arrays entweder entlang der zwei ersten oder der zwei letzten Indizes bestätigt werden (für meine Zwecke ist dies eine Simulation des Ladens von 500 Bildern mit einer Größe von 512 x 512 Pixeln):

%Vor%

Benchmark

%Vor%

So weit, so gut. Wenn ich jedoch Arrays entlang der letzten und drittletzten Dimension lade, ist das fast so schnell wie das Laden in die beiden letzten Dimensionen.

%Vor%

Benchmark

%Vor%
  • Warum ist last_and_third_last() so nahe bei last_and_second_last() im Vergleich zu second_and third_last() ?
  • Was ist eine gute Möglichkeit zu visualisieren, warum der letzte Index wesentlich mehr zählt als der zweitletzte Index in Bezug auf die Zugriffsgeschwindigkeit?
Joel Ostblom 22.05.2017, 14:31
quelle

4 Antworten

4

Ich werde versuchen, die Indexierung zu veranschaulichen, ohne Details des Prozessor-Caching usw. zu nennen.

Lässt ein kleines 3D-Array mit verschiedenen Elementwerten erstellen:

%Vor%

ravel betrachtet es als 1d-Array und zeigt uns, wie die Werte im Speicher angeordnet sind. (Dies ist übrigens mit der standardmäßigen Reihenfolge C )

%Vor%

Wenn ich auf die 1. Dimension indexiere, erhalte ich 2 * 3 Werte, einen zusammenhängenden Block der obigen Liste:

%Vor%

Die Indizierung an der letzten Stelle ergibt 4 Werte, die aus dem Array ausgewählt sind - Ich habe .. hinzugefügt, um das hervorzuheben

%Vor%

Die Indizierung in der Mitte ergibt 2 zusammenhängende Unterblöcke, d. h. 2 Zeilen von X .

%Vor%

Mit den Berechnungen strides und shape kann numpy gleichzeitig (etwa) auf ein beliebiges Element in X zugreifen. Und im Fall X[:,:,i] muss es das tun. Die 4 Werte sind über den Datenpuffer verteilt.

Wenn es jedoch auf zusammenhängende Blöcke zugreifen kann, z. B. in X[i,:,:] , kann es mehr von der Aktion an kompilierten und Prozessorcode auf niedriger Ebene delegieren. Mit X[:,i,:] sind diese Blöcke nicht ganz so groß, können aber trotzdem groß genug sein, um einen großen Unterschied zu machen.

In Ihrem Testfall wiederholt [n,:,:] 500 Mal auf 512 * 512 Elementblöcken.

[:,n,:] muss diesen Zugriff in 512 Blöcke von je 512 teilen.

[:,:,n] muss 500 x 512 x 512 einzelne Zugriffe ausführen.

Ich frage mich, ob die Arbeit mit uint16 den Effekt übertreibt. In einer anderen Frage haben wir gerade gezeigt, dass die Berechnung mit float16 sehr viel langsamer ist (bis zu 10x), weil der Prozessor (und der Compiler) darauf abgestimmt ist, mit 32- und 64-Bit-Zahlen zu arbeiten. Wenn der Prozessor darauf eingestellt ist, Blöcke mit 64-Bit-Nummern zu verschieben, kann das Verschieben einer isolierten 16-Bit-Nummer viel zusätzliche Verarbeitung erfordern. Es wäre so, als würde man aus einem Dokument Wort-für-Wort kopieren und einfügen, wenn das Kopieren zeilenweise weniger Tastenanschläge pro Kopie erfordert.

Die genauen Details sind im Prozessor, im Betriebssystem und im Compiler sowie in numpy code verborgen, aber hoffentlich gibt dies ein Gefühl dafür, warum Ihr mittlerer Fall dem Optimum viel näher kommt als dem schlimmsten Fall.

Beim Testen: Wenn imgs auf a.dtype gesetzt wird, wird die Geschwindigkeit in allen Fällen etwas verringert. Das 'uint16' verursacht also keine besonderen Probleme.

Warum funktioniert 'numpy.einsum' schneller mit 'float32' als 'float16' oder 'uint16'?

    
hpaulj 22.05.2017, 20:09
quelle
4

Numpys Arrays sind auf c und c ++ aufgebaut, so dass wir über Dinge wie Cache-Zeilen nachdenken, wenn wir sie an ihre absoluten Grenzen treiben. In last_and_second_last(): und last_and_third_last(): liest man mehr als ein einzelnes Byte entlang der letzten Achse, so dass eine ganze Cache-Zeile gleichzeitig verwendet wird (16, da Ihre letzte Achse 1024 Bytes lang ist). In second_and_third_last() muss eine ganze Cache-Zeile kopiert werden, um einen einzelnen Wert in der letzten Achse zu lesen (oder zu schreiben). Moderne c-Compiler (und andere: fortran usw.) werden verschachtelte Schleifen verwenden, die in der falschen Reihenfolge auf Speicher zugreifen und sie stillschweigend neu ordnen, um die Cache-Nutzung zu optimieren, aber Python kann dies nicht tun.

Beispiel:

  • Angenommen, Sie haben einen sehr einfachen Prozessor, dessen Cache 4 Wörter breit ist.
  • Ihr Array ist 4x4x4 arr = np.arange(64).reshape([4,4,4])
  • Wenn Sie auf arr[i,j,:] zugreifen möchten, können Sie alle gleichzeitig im Cache laden (zB [0,1,2,3] )
  • Wenn Sie auf arr[i,:,k] zugreifen möchten
    • Der Prozessor lädt zuerst arr[i,0,:] und liest [k] aus diesem Array von 4
    • Dann wird arr[i,1,:] geladen und [k] von diesem Array von 4
    • gelesen
    • usw. ...
Aaron 22.05.2017 15:03
quelle
2

Der entscheidende Punkt hier ist nicht, dass es die letzte Achse ist, sondern dass es die Achse ax ist, wo imgs.strides[ax] == imgs.dtype.itemsize - das heißt, der Speicher ist zusammenhängend entlang dieser Achse. Das Standardverhalten besteht darin, dies auf die letzte Achse anzuwenden, aber gehen Sie nicht davon aus - Sie werden das umgekehrte Verhalten mit imgs.T sehen (da dies eine Ansicht erzeugt und das strides -Array umkehrt)

Wenn NumPy feststellt, dass Achsen zusammenhängend sind, verwendet es memcpy für die gesamte Dimension, was die Compiler erheblich optimieren. In anderen Fällen kann NumPy nur ein Element gleichzeitig speichern

    
Eric 22.05.2017 16:14
quelle
0

Ein Schlüssel für mich war zu verstehen, dass Zeilen in einem C-geordneten numpy-Array aneinandergefügt werden, um einen fortlaufenden Block / Puffer im Speicher zu bilden, ähnlich dem Layout @hpaulj, das mit .ravel() angezeigt wurde. Mehr darüber zu erfahren, wie Arrays in C funktionieren, half weiter mit diesem Verständnis, insbesondere mit diesen drei Ressourcen:

Da es sehr effizient ist, kontinuierlich im Speicher angeordnete Elemente abzurufen, werden die kostspieligen Teile des Zugriffs auf ein Array zu Suchvorgängen, bei denen Teile der fortlaufenden Speicherabschnitte übersprungen werden, um das Lesen an einem anderen Speicherort fortzusetzen. Aarons Antwort beschreibt Gründe, warum dies eine teure Operation ist.

Wie @hpaulj sagte, macht der [n,:,:] -Ansatz die geringste Menge an Nachschlagevorgängen und der [:,:,n] -Ansatz macht am meisten , was erklärt, warum diese Methode deutlich hinter den beiden anderen zurückbleibt :

%Vor%     
Joel Ostblom 25.05.2017 03:58
quelle