Вокруг есть числаxpr , numba и cython , цель этого ответа - принять во внимание эти возможности.
Но сначала давайте констатируем очевидное: независимо от того, как вы отображаете Python-функцию на массив numpy, она остается функцией Python, что означает для каждой оценки:
- элемент numpy-array должен быть преобразован в объект Python (например,
Float
).
- все вычисления выполняются с Python-объектами, что означает наличие накладных расходов на интерпретатор, динамическую диспетчеризацию и неизменяемые объекты.
То, какой механизм используется для циклического прохождения массива, не играет большой роли из-за упомянутых выше издержек - он работает намного медленнее, чем использование встроенной функциональности numpy.
Давайте посмотрим на следующий пример:
# 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
выбран в качестве представителя класса подходов чисто Python функции. Используя perfplot
(см. Код в приложении к этому ответу), мы получаем следующее время выполнения:
Мы можем видеть, что numpy-подход в 10-100 раз быстрее, чем в чистой версии Python. Вероятно, снижение производительности при больших размерах массивов связано с тем, что данные больше не помещаются в кэш.
Стоит также упомянуть, что он vectorize
также использует много памяти, поэтому часто использование памяти является узким местом (см. Соответствующий вопрос SO ). Также обратите внимание, что в документации numpy np.vectorize
говорится, что она «предоставляется в основном для удобства, а не для производительности».
При желании использовать другие инструменты, кроме написания C-расширения с нуля, существуют следующие возможности:
Часто можно услышать, что производительность NumPy настолько хороша, насколько это возможно, потому что это чистый C под капотом. Тем не менее, есть много возможностей для совершенствования!
Векторизованная numpy-версия использует много дополнительной памяти и обращений к памяти. Numexp-library пытается упорядочить numpy-массивы и таким образом получить лучшее использование кэша:
# 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")
Приводит к следующему сравнению:
Я не могу объяснить все на графике выше: вначале мы видим большие издержки для библиотеки numbersxpr, но поскольку она лучше использует кэш, она примерно в 10 раз быстрее для больших массивов!
Другой подход состоит в том, чтобы выполнить jit-компиляцию функции и, таким образом, получить настоящий UFunc на чистом C. Это подход Нумбы:
# 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
Это в 10 раз быстрее, чем оригинальный numpy-подход:
Однако задача смущающе распараллеливается, поэтому мы также можем использовать ее prange
для параллельного вычисления цикла:
@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
Как и ожидалось, параллельная функция медленнее для небольших входов, но быстрее (почти в 2 раза) для больших размеров:
В то время как numba специализируется на оптимизации операций с numpy-массивами, Cython является более общим инструментом. Извлечь ту же производительность, что и с numba, сложнее - часто она снижается до llvm (numba) по сравнению с локальным компилятором (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 приводит к несколько более медленным функциям:
Вывод
Очевидно, что тестирование только для одной функции ничего не доказывает. Также следует помнить, что для выбранной функции-примера пропускная способность памяти была узким местом для размеров, превышающих 10 ^ 5 элементов - таким образом, мы имели одинаковую производительность для numba, figurexpr и cython в этой области.
В конце концов, окончательный ответ зависит от типа функции, аппаратного обеспечения, Python-распределения и других факторов. Например , Анаконда-распределение использует Intel, VML для функций Numpy и , таким образом , превосходит по Numba (если он не использует SVML, увидеть этот SO-пост ) легко для трансцендентных функций , такие как exp
, sin
, cos
и аналогичного - смотрите , например , следующий SO-пост .
Тем не менее, исходя из этого исследования и моего опыта, я до сих пор утверждаю, что нумба кажется самым простым инструментом с наилучшими характеристиками, если не задействованы трансцендентные функции.
График времени прохождения с перфлот-пакетом :
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)'
)