Es gibt Numexpr , Numba und Cython . Das Ziel dieser Antwort ist es, diese Möglichkeiten zu berücksichtigen.
Aber lassen Sie uns zuerst das Offensichtliche sagen: Egal wie Sie eine Python-Funktion einem Numpy-Array zuordnen, es bleibt eine Python-Funktion, das heißt für jede Auswertung:
- Das numpy-Array-Element muss in ein Python-Objekt konvertiert werden (z
Float
. B. a ).
- Alle Berechnungen werden mit Python-Objekten durchgeführt, was bedeutet, dass der Overhead von Interpreter, dynamischem Versand und unveränderlichen Objekten anfällt.
Welche Maschinerie verwendet wird, um das Array tatsächlich zu durchlaufen, spielt aufgrund des oben erwähnten Overheads keine große Rolle - sie bleibt viel langsamer als die Verwendung der integrierten Funktionalität von numpy.
Schauen wir uns das folgende Beispiel an:
# numpy-functionality
def f(x):
return x+2*x*x+4*x*x*x
# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"
np.vectorize
wird als Vertreter der reinen Python-Funktionsklasse von Ansätzen ausgewählt. Mit perfplot
(siehe Code im Anhang dieser Antwort) erhalten wir folgende Laufzeiten:
Wir können sehen, dass der Numpy-Ansatz 10x-100x schneller ist als die reine Python-Version. Der Leistungsabfall bei größeren Array-Größen ist wahrscheinlich darauf zurückzuführen, dass Daten nicht mehr in den Cache passen.
Erwähnenswert ist auch, dass vectorize
auch viel Speicher benötigt wird, so dass die Speichernutzung häufig der Flaschenhals ist (siehe verwandte SO-Frage ). Beachten Sie auch, dass in der Dokumentation von numpy angegeben ist np.vectorize
, dass es "in erster Linie der Einfachheit halber und nicht der Leistung dient".
Wenn Leistung gewünscht wird, sollten andere Tools verwendet werden. Neben dem Schreiben einer C-Erweiterung von Grund auf gibt es folgende Möglichkeiten:
Man hört oft, dass die Numpy-Performance so gut ist wie es nur geht, weil es reines C unter der Haube ist. Dennoch gibt es viel Raum für Verbesserungen!
Die vektorisierte Numpy-Version verwendet viel zusätzlichen Speicher und Speicherzugriffe. Die Numexp-Bibliothek versucht, die Numpy-Arrays zu kacheln und so eine bessere Cache-Auslastung zu erzielen:
# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
return ne.evaluate("x+2*x*x+4*x*x*x")
Führt zu folgendem Vergleich:
Ich kann nicht alles in der obigen Darstellung erklären: Wir können am Anfang einen größeren Overhead für die numexpr-Bibliothek sehen, aber da der Cache besser genutzt wird, ist er für größere Arrays etwa zehnmal schneller!
Ein anderer Ansatz besteht darin, die Funktion zu kompilieren und so einen echten UFunc mit reinem C zu erhalten. Dies ist Numbas Ansatz:
# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
return x+2*x*x+4*x*x*x
Es ist zehnmal schneller als der ursprüngliche Numpy-Ansatz:
Die Aufgabe ist jedoch peinlich parallelisierbar, sodass wir sie auch verwenden könnten prange
, um die Schleife parallel zu berechnen:
@nb.njit(parallel=True)
def nb_par_jitf(x):
y=np.empty(x.shape)
for i in nb.prange(len(x)):
y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y
Wie erwartet ist die Parallelfunktion bei kleineren Eingängen langsamer, bei größeren jedoch schneller (fast Faktor 2):
Während sich numba auf die Optimierung von Operationen mit numpy-Arrays spezialisiert hat, ist Cython ein allgemeineres Werkzeug. Es ist komplizierter, die gleiche Leistung wie bei numba zu extrahieren - oft liegt es an llvm (numba) gegenüber dem lokalen Compiler (gcc / MSVC):
%%cython -c=/openmp -a
import numpy as np
import cython
#single core:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_f(double[::1] x):
y_out=np.empty(len(x))
cdef Py_ssize_t i
cdef double[::1] y=y_out
for i in range(len(x)):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
#parallel:
from cython.parallel import prange
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_par_f(double[::1] x):
y_out=np.empty(len(x))
cdef double[::1] y=y_out
cdef Py_ssize_t i
cdef Py_ssize_t n = len(x)
for i in prange(n, nogil=True):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
Cython führt zu etwas langsameren Funktionen:
Fazit
Offensichtlich beweist das Testen nur für eine Funktion nichts. Man sollte auch bedenken, dass für das gewählte Funktionsbeispiel die Bandbreite des Speichers der Flaschenhals für Größen größer als 10 ^ 5 Elemente war - daher hatten wir in dieser Region die gleiche Leistung für numba, numexpr und cython.
Letztendlich hängt die endgültige Antwort von der Art der Funktion, der Hardware, der Python-Verteilung und anderen Faktoren ab. Zum Beispiel Anaconda-Distribution verwendet Intels VML für Funktionen numpy ist und damit übertrifft numba (es sei denn , es SVML verwendet, finden Sie diese SO-post ) leicht für transzendente Funktionen wie exp
, sin
, cos
und ähnlich - siehe zum Beispiel die folgenden SO-Post .
Aufgrund dieser Untersuchung und meiner bisherigen Erfahrungen würde ich jedoch feststellen, dass Numba das einfachste Werkzeug mit der besten Leistung zu sein scheint, solange keine transzendentalen Funktionen beteiligt sind.
Laufzeit mit Perfplot-Paket zeichnen :
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(0,24)],
kernels=[
f,
vf,
ne_f,
nb_vf, nb_par_jitf,
cy_f, cy_par_f,
],
logx=True,
logy=True,
xlabel='len(x)'
)