Навколо є numexpr , numba та cython , метою цієї відповіді є врахування цих можливостей.
Але спершу давайте констатуємо очевидне: незалежно від того, як відображати функцію Python на масиві numpy, вона залишається функцією Python, що означає для кожної оцінки:
- елемент numpy-масив повинен бути перетворений на об'єкт Python (наприклад, a
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
вибирається як представник класу функцій чистого пітона. Використовуючи perfplot
(див. Код у додатку цієї відповіді), ми отримуємо такі тривалість виконання:
Ми можемо бачити, що numpy-підхід на 10x-100x швидший, ніж у чистому варіанті python. Зниження продуктивності для більших розмірів масиву, мабуть, пояснюється тим, що дані більше не підходять до кешу.
Варто також зазначити, що vectorize
також використовується багато пам'яті, тому часто використання пам'яті - це шийка (див. Пов’язане SO-питання ). Також зауважте, що документація numpy про np.vectorize
те, що вона "надається в першу чергу для зручності, а не для виконання".
Інші інструменти слід використовувати, коли потрібна продуктивність, окрім написання розширення C з нуля, є такі можливості:
Часто можна почути, що продуктивність настільки ж хороша, наскільки це виходить, адже це чисто C під кришкою. І все ж є багато можливостей для вдосконалення!
Векторизована numpy-версія використовує багато додаткової пам'яті та доступу до пам'яті. Бібліотека Numexp намагається встановити рядки 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")
Приводить до такого порівняння:
Я не можу пояснити все на сюжеті вище: ми можемо побачити більший накладний обсяг для бібліотеки numexpr на початку, але оскільки він краще використовує кеш, це приблизно в 10 разів швидше для великих масивів!
Інший підхід полягає в тому, щоб jit-компілювати функцію і таким чином отримати справжній чистий-C UFunc. Це підхід 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
Це в 10 разів швидше, ніж оригінальний підхід:
Однак завдання є незручно паралельним, таким чином ми також могли б використати 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, numexpr та cython в цьому регіоні.
Зрештою, остаточна відповідь залежить від типу функції, апаратного забезпечення, розподілу Python та інших факторів. Наприклад, Anaconda-дистрибуція використовує VML від Intel для функцій numpy і, таким чином, перевершує numba (якщо тільки він не використовує SVML, дивіться цю публікацію SO ) легко для трансцендентних функцій , такі як exp
, sin
, cos
і аналогічним - дивіться , наприклад , наступний SO-пост .
Проте, з цього дослідження та з мого досвіду, я зазначив, що numba, здається, є найпростішим інструментом з найкращою продуктивністю, доки не задіяні трансцендентні функції.
Складання графіків роботи за допомогою пакету 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)'
)