Wokół jest numexpr , numba i cython , celem tej odpowiedzi jest wzięcie pod uwagę tych możliwości.
Ale najpierw określmy oczywiste: bez względu na to, jak mapujesz funkcję Pythona na tablicę numpy, pozostaje ona funkcją Pythona, co oznacza dla każdej oceny:
- element numpy-array musi zostać przekonwertowany na obiekt Python (np
Float
).
- wszystkie obliczenia są wykonywane za pomocą obiektów Python, co oznacza, że mamy narzut interpretera, dynamicznej wysyłki i niezmiennych obiektów.
Tak więc, która maszyna jest używana do przechodzenia przez tablicę, nie odgrywa dużej roli z powodu wspomnianego powyżej narzutu - pozostaje znacznie wolniejsza niż korzystanie z wbudowanej funkcjonalności numpy.
Rzućmy okiem na następujący przykład:
# 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
jest wybierany jako reprezentant klasy metod czysto pythonowych. Używając perfplot
(patrz kod w załączniku do tej odpowiedzi) otrzymujemy następujące czasy działania:
Widzimy, że podejście numpy jest 10x-100x szybsze niż wersja czysto pythonowa. Spadek wydajności w przypadku większych rozmiarów macierzy jest prawdopodobnie spowodowany tym, że dane nie pasują już do pamięci podręcznej.
Warto również wspomnieć, że vectorize
również zużywa dużo pamięci, więc często użycie pamięci to szyjka butelki (patrz powiązane pytanie SO ). Zauważ też, że dokumentacja tego numpy nanp.vectorize
stwierdza, że jest „przede wszystkim dla wygody, a nie dla wydajności”.
Gdy pożądana jest wydajność, należy użyć innych narzędzi, oprócz napisania rozszerzenia C od zera, istnieją następujące możliwości:
Często słyszy się, że numpy-wydajność jest tak dobra, jak to tylko możliwe, ponieważ pod maską jest czystym C. Ale jest jeszcze wiele do zrobienia!
Wektoryzowana wersja numpy wykorzystuje wiele dodatkowej pamięci i dostęp do pamięci. Biblioteka Numexp próbuje kafelkować tablice numpy, a tym samym uzyskać lepsze wykorzystanie pamięci podręcznej:
# 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")
Prowadzi do następującego porównania:
Nie mogę wyjaśnić wszystkiego na powyższym wykresie: na początku możemy zobaczyć większy narzut dla biblioteki numexpr, ale ponieważ lepiej wykorzystuje pamięć podręczną, dla większych tablic jest około 10 razy szybszy!
Innym podejściem jest skompilowanie funkcji przez jit, a tym samym uzyskanie prawdziwego UFunc w czystym C. Oto podejście Numba:
# 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
Jest 10 razy szybszy niż oryginalne podejście numpy:
Jednak zadanie jest kłopotliwie równoległe, dlatego moglibyśmy również użyć prange
do równoległego obliczenia pętli:
@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
Zgodnie z oczekiwaniami funkcja równoległa jest wolniejsza dla mniejszych wejść, ale szybsza (prawie współczynnik 2) dla większych rozmiarów:
Podczas gdy numba specjalizuje się w optymalizacji operacji za pomocą tablic numpy, Cython jest bardziej ogólnym narzędziem. Bardziej skomplikowane jest wyodrębnienie tej samej wydajności, co w przypadku numba - często jest to zależne od llvm (numba) vs lokalnego kompilatora (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 powoduje nieco wolniejsze funkcje:
Wniosek
Oczywiście testowanie tylko jednej funkcji niczego nie dowodzi. Należy również pamiętać, że dla wybranego przykładu funkcji przepustowość pamięci była szyjką butelki dla rozmiarów większych niż 10 ^ 5 elementów - dlatego mieliśmy taką samą wydajność dla numba, numexpr i cython w tym regionie.
Ostatecznie ostateczna odpowiedź zależy od rodzaju funkcji, sprzętu, dystrybucji Pythona i innych czynników. Na przykład Anaconda-dystrybucji używa Intela VML dla funkcji NumPy i tym samym przewyższa Numba (chyba że korzysta SVML, zobacz ten SO-post ) łatwo za transcendentalne funkcje jak exp
, sin
, cos
i podobne - patrz np następującym SO-post .
Jednak na podstawie tego dochodzenia i z dotychczasowego doświadczenia powiedziałbym, że numba wydaje się najłatwiejszym narzędziem o najlepszym działaniu, o ile nie są zaangażowane żadne funkcje transcendentalne.
Rysowanie czasów pracy za pomocą pakietu perfplot:
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)'
)