Leistung in verschiedenen Vektorisierungsmethoden in numpy

8

Ich wollte die Leistung von Vektorisierungscode in Python testen:

%Vor%

Der Code gibt die folgende Ausgabe aus:

%Vor%

Der Leistungsunterschied in der ersten und zweiten Funktion ist nicht überraschend. Aber ich war überrascht, dass die 3. Funktion deutlich langsamer ist als die anderen Funktionen.

Ich bin viel vertrauter beim Vektorisieren von Code in C als in Python und die 3. Funktion ist C-ähnlicher - führt eine for-Schleife aus und verarbeitet 4 Zahlen in einer Anweisung in jeder Schleife. Nach meinem Verständnis ruft numpy eine C-Funktion auf und vektorisiert dann den Code in C. Wenn das der Fall ist, übergibt mein Code auch 4 Zahlen an jeweils eine Zahl. Der Code sollte nicht besser funktionieren, wenn ich mehrere Zahlen gleichzeitig übergebe. Warum ist es viel langsamer? Liegt es am Overhead beim Aufruf einer numpigen Funktion?

Außerdem ist der Grund, warum ich überhaupt die dritte Funktion entwickelt habe, die, dass ich mir Sorgen um die Leistung der großen Speicherzuweisung an x in func1 mache.

Ist meine Sorge gültig? Warum und wie kann ich es verbessern oder warum nicht?

Vielen Dank im Voraus.

Bearbeiten:

Aus Neugier, obwohl es meinen ursprünglichen Zweck für die Erstellung der dritten Version besiegt, habe ich Roganjoshs Vorschlag untersucht und den folgenden Schnitt versucht.

%Vor%

Die Ausgabe:

%Vor%

Es gibt eine Verbesserung, aber immer noch eine große Lücke im Vergleich zu den anderen Funktionen.

Liegt das daran, dass x[i:i+4] immer noch ein neues Array erstellt?

Bearbeiten 2:

Ich habe den Code wieder entsprechend Daniels Vorschlag geändert.

%Vor%

Die Ausgabe:

%Vor%

Es gibt eine weitere Beschleunigung. Die Deklaration von numpy Arrays ist also definitiv ein Problem. Nun sollte in func3 nur eine Array-Deklaration vorhanden sein, aber die Zeit ist noch viel langsamer. Ist es wegen des Overheads, numpy Arrays aufzurufen?

    
Batrobin 06.05.2017, 17:35
quelle

3 Antworten

9

Es scheint, dass Sie hauptsächlich an dem Unterschied zwischen Ihrer Funktion 3 im Vergleich zu den reinen NumPy (Funktion 1) und Python (Funktion 2) -Ansätzen interessiert sind. Die Antwort ist ziemlich einfach (besonders wenn Sie sich Funktion 4 anschauen):

  • NumPy-Funktionen haben einen "großen" konstanten Faktor.

Sie benötigen normalerweise mehrere tausend Elemente, um in das Regime zu kommen, in dem die Laufzeit von np.sum tatsächlich von der Anzahl der Elemente im Array abhängt. Mit IPython und Matplotlib (das Diagramm befindet sich am Ende der Antwort) können Sie die Laufzeitabhängigkeit leicht überprüfen:

%Vor%

Die Ergebnisse für np.sum (verkürzt) sind ziemlich interessant:

%Vor%

Es scheint der konstante Faktor ist ungefähr 20µs auf meinem Computer) und es braucht ein Array mit 16384 tausend Elementen, um diese Zeit zu verdoppeln. Das Timing für die Funktionen 3 und 4 ist also meist ein Timing-Multiplikativ des konstanten Faktors.

In Funktion 3 geben Sie den konstanten Faktor 2 Mal ein, einmal mit np.sum und einmal mit np.arange . In diesem Fall ist arange ziemlich billig, da jedes Array die gleiche Größe hat, so dass NumPy & amp; Python & amp; Ihr Betriebssystem wird wahrscheinlich den Speicher des Arrays der letzten Iteration wiederverwenden. Aber selbst das braucht Zeit (ungefähr 2µs für sehr kleine Arrays auf meinem Computer).

Allgemeiner: Um Engpässe zu erkennen, sollten Sie immer die Funktionen profilieren!

Ich zeige die Ergebnisse für die Funktionen mit line-profiler . Daher habe ich die Funktionen etwas geändert, so dass sie nur eine Operation pro Zeile ausführen:

%Vor%

Ergebnisse:

%Vor%

Ich werde nicht auf die Details der Ergebnisse eingehen, aber wie Sie sehen können, ist np.sum definitiv der Flaschenhals in func3 und func4 . Ich vermutete bereits, dass np.sum der Flaschenhals ist, bevor ich die Antwort geschrieben habe, aber diese Linienprofile bestätigen tatsächlich, dass es der Engpass ist.

Was zu einem sehr wichtigen Fakt bei der Verwendung von NumPy führt:

  • Wissen, wann man es benutzt! Kleine Arrays sind es (meistens) nicht wert.
  • Kenne die NumPy-Funktionen und verwende sie einfach. Sie verwenden (wenn verfügbar) bereits Compiler-Optimierungs-Flags, um Schleifen zu loeschen.

Wenn Sie wirklich glauben, dass ein Teil zu langsam ist, können Sie Folgendes verwenden:

  • NumPy's C API und verarbeite das Array mit C (kann mit Cython wirklich einfach sein, aber du kannst es auch manuell machen)
  • Numba (basierend auf LLVM).

Aber im Allgemeinen können Sie NumPy wahrscheinlich nicht für mittelgroße Arrays (mehrere tausend Einträge und mehr) schlagen.

Visualisierung der Zeiten:

%Vor%

Die Plots sind Log-Log, ich denke, es war der beste Weg, um die Daten zu visualisieren, da sie sich um mehrere Größenordnungen erstreckt (ich hoffe nur, dass es immer noch verständlich ist).

Das erste Diagramm zeigt, wie viel Zeit es braucht, um sum zu machen:

Das zweite Diagramm zeigt die durchschnittliche Zeit, die für die sum benötigt wird, geteilt durch die Anzahl der Elemente im Array. Dies ist nur eine weitere Möglichkeit, die Daten zu interpretieren:

    
MSeifert 06.05.2017, 18:54
quelle
4

Basierend auf den Tests (siehe nächste) scheint es, dass Sie vom Funktionsaufruf-Overhead übertroffen werden. Zusammen mit der vektorisierten Fähigkeit von NumPy-Funktionen / Werkzeugen müssen wir ihm genügend Daten für das Knirschen geben. Mit func3 geben wir nur 4 Elemente pro Aufruf an np.sum .

Lassen Sie uns den pro-Aufruf-Overhead für np.sum untersuchen. Hier ist np.sum beginnend mit der Summierung von keinem, einem Element und weiter -

%Vor%

und so weiter.

Somit würden wir für diese Tests ein Minimum von ca. 1.6 u-sec pro Aufruf von np.sum im System-Setup erhalten.

Sehen wir uns an, wie die Skalaraddition mit dem Additionsoperator funktioniert -

%Vor%

Dies ist ungefähr 25x schneller als der Overhead pro Aufruf für np.sum .

Die nächstliegende Idee ist es, zu testen, wie func3 mit mehr Datenverarbeitung in np.sum funktioniert.

Modified func3 (die Version, die Slicing verwendet), um eine variable Datengröße für die Iterationsprozedur zu haben:

%Vor%

Beginnend mit einem scale_factor = 4 wie ursprünglich verwendet -

%Vor%

Ja, func3 ist langsam.

Nun geben wir mehr Daten pro Aufruf an np.sum , d. h. erhöhen scale_factor -

%Vor%

und so weiter, bis wir die gesamten Daten an np.sum für das maximale Leistungslimit mit np.sum und minimalem Anruf-Overhead einspeisen.

    
Divakar 06.05.2017 18:46
quelle
3

Zunächst würde niemand die dritte Variante in C schreiben, weil der Compiler die notwendigen Optimierungen vornehmen sollte.

Also nimm die erste, du hast zwei Kreationen von numpy Arrays (arange und * 2) und eine Summierung. Das Erstellen komplexer Objekte wie numpy Arrays benötigt einige Zeit, aber jede Vektoroperation wird in C-Code und sehr schnell geschrieben.

Der zweite verwendet nur primitive Python-Operationen (etwa 3000, Iteration, Multiplikation und Summierung), die in C und sehr schnell geschrieben sind.

Dritte Variante Sie erstellen etwa 2 * 250 numpy Arrays (eine vergleichsweise langsame Operation), was zu einer 100 mal langsameren Ausführungsgeschwindigkeit führt, als wenn nur zwei numpige Arrays erstellt werden.

Wenn Sie Bedenken hinsichtlich der Speichernutzung haben, sollten Sie Inline-Operationen verwenden, die nur ein Array erstellen:

%Vor%

Wenn Sie immer noch zu viel Speicher benötigen, teilen Sie Ihre Operationen in so große Stücke wie möglich.

    
Daniel 06.05.2017 18:11
quelle