#python #arrays #performance #numpy #cython
#python #массивы #Производительность #numpy #cython
Вопрос:
Рассмотрим следующий код Cython :
cimport cython
cimport numpy as np
import numpy as np
@cython.boundscheck(False)
@cython.wraparound(False)
def test_memoryview(double[:] a, double[:] b):
cdef int i
for i in range(a.shape[0]):
a[i] = b[i]
@cython.boundscheck(False)
@cython.wraparound(False)
def test_numpy(np.ndarray[double, ndim=1] a, np.ndarray[double, ndim=1] b):
cdef int i
for i in range(a.shape[0]):
a[i] = b[i]
def test_numpyvec(a, b):
a = b
def gendata(nb=40000000):
a = np.random.random(nb)
b = np.random.random(nb)
return a, b
Запуск его в интерпретаторе дает результат (после нескольких запусков для прогрева кэша) :
In [14]: %timeit -n 100 test_memoryview(a, b)
100 loops, best of 3: 148 ms per loop
In [15]: %timeit -n 100 test_numpy(a, b)
100 loops, best of 3: 159 ms per loop
In [16]: %timeit -n 100 test_numpyvec(a, b)
100 loops, best of 3: 124 ms per loop
# See answer below :
In [17]: %timeit -n 100 test_raw_pointers(a, b)
100 loops, best of 3: 129 ms per loop
Я пробовал это с разными размерами набора данных, и неизменно векторизованная функция NumPy выполнялась быстрее, чем скомпилированный код Cython, в то время как я ожидал, что Cython будет на одном уровне с векторизованным NumPy с точки зрения производительности.
Я забыл оптимизацию в своем коде Cython? Использует ли NumPy что-то (BLAS?), Чтобы ускорить выполнение таких простых операций? Могу ли я улучшить производительность этого кода?
Обновление: версия необработанного указателя, похоже, соответствует NumPy. Так что, очевидно, есть некоторые накладные расходы при использовании представления памяти или индексации NumPy.
Комментарии:
1. 10 циклов: вы действительно запускаете тесты производительности только 10 раз, чтобы получить среднее значение? Если это так, то нормальная дисперсия может быть больше, чем вы пытаетесь измерить. Вместо этого попробуйте 100000 раз.
2. Это Python 2.x? Если да,
range
то это может объяснить некоторую разницу3. @AaronDigulla: Я обновил вопрос таймингами для 100 запусков
4. @MrE: У меня создалось впечатление, что Cython автоматически преобразовал использование циклов
range
в C. Я был неправ?5. В зависимости от вашего оборудования и версии numpy некоторые базовые математические операции могут использовать инструкции SSE2 и, следовательно, выполняться в два раза быстрее
double
или в 4 раза быстрееfloat
, чем в ванильной реализации C / Cython.
Ответ №1:
Другой вариант — использовать необработанные указатели (и глобальные директивы, чтобы избежать повторения @cython...
):
#cython: wraparound=False
#cython: boundscheck=False
#cython: nonecheck=False
#...
cdef ctest_raw_pointers(int n, double *a, double *b):
cdef int i
for i in range(n):
a[i] = b[i]
def test_raw_pointers(np.ndarray[double, ndim=1] a, np.ndarray[double, ndim=1] b):
ctest_raw_pointers(a.shape[0], amp;a[0], amp;b[0])
Комментарии:
1. Хорошая идея, я обновлю вопрос таймингами из этой функции!
2. Смотрите мое обновление. По-видимому, необработанные указатели соответствуют векторизованной версии NumPy. Я собираюсь исследовать это дальше, и если нет лучшего варианта, я приму ваш ответ.
3. На самом деле я этого не делал, поэтому я собираюсь принять ваш ответ, спасибо, что напомнили мне!
Ответ №2:
На моей машине разница не такая большая, но я могу почти устранить ее, изменив функции numpy и просмотра памяти следующим образом
@cython.boundscheck(False)
@cython.wraparound(False)
def test_memoryview(double[:] a, double[:] b):
cdef int i, n=a.shape[0]
for i in range(n):
a[i] = b[i]
@cython.boundscheck(False)
@cython.wraparound(False)
def test_numpy(np.ndarray[double] a, np.ndarray[double] b):
cdef int i, n=a.shape[0]
for i in range(n):
a[i] = b[i]
и затем, когда я компилирую вывод C из Cython, я использую флаги -O3
и -march=native
.
Это, по-видимому, указывает на то, что разница во времени возникает из-за использования разных оптимизаций компилятора.
Я использую 64-разрядную версию MinGW и NumPy 1.8.1. Ваши результаты, вероятно, будут отличаться в зависимости от версий вашего пакета, аппаратного обеспечения, платформы и компилятора.
Если вы используете Cython magic от IPython notebook, вы можете принудительно обновить дополнительные флаги компилятора, заменив %%cython
на %%cython -f -c=-O3 -c=-march=native
Если вы используете стандартный setup.py для вашего модуля cython вы можете указать extra_compile_args
аргумент при создании объекта расширения , которому вы передаете distutils.setup
.
Примечание: я удалил ndim=1
флаг при указании типов для массивов NumPy, потому что в этом нет необходимости. В любом случае это значение по умолчанию равно 1.
Комментарии:
1. Я использую setup.py файл, потому что я не знал о магии IPython, что, кстати, довольно приятно! IIRC distutils по умолчанию использует значение -O2 при компиляции расширений, возможно, это то, что здесь происходит. Я рассмотрю это в понедельник!
Ответ №3:
Изменение, которое немного увеличивает скорость, заключается в указании шага:
def test_memoryview_inorder(double[::1] a, double[::1] b):
cdef int i
for i in range(a.shape[0]):
a[i] = b[i]
Комментарии:
1. У меня есть двумерный массив, и я попытался указать
double[::1, ::1] b
, но мне сказали: «Невозможно указать массив, который является непрерывным как на C, так и на Fortran». Запись простоdouble[:, ::1] b
компилируется. Есть ли способ использовать ваши ответы для двух измерений?2. @ThomasAhle docs.cython.org/en/latest/src/userguide / … , я думаю
double[:, ::1]
(илиdouble[::1, :]
) должно быть хорошо.3. Это дало улучшение в 2 раза в моем коде. Можете ли вы направить меня куда-нибудь, где я могу узнать о том, что происходит?