Скорость вращения шпинделя против Китона
У меня есть код анализа, который выполняет некоторые тяжелые числовые операции с использованием numpy. Просто для любопытства, попытался скомпилировать его с помощью cython с небольшими изменениями, а затем я переписал его с использованием циклов для части numpy.
К моему удивлению, код, основанный на циклах, был намного быстрее (8x). Я не могу опубликовать полный код, но я собрал очень простое несвязанное вычисление, которое показывает подобное поведение (хотя разница во времени не такая большая):
Версия 1 (без cython)
import numpy as np
def _process(array):
rows = array.shape[0]
cols = array.shape[1]
out = np.zeros((rows, cols))
for row in range(0, rows):
out[row, :] = np.sum(array - array[row, :], axis=0)
return out
def main():
data = np.load('data.npy')
out = _process(data)
np.save('vianumpy.npy', out)
Версия 2 (создание модуля с cython)
import cython
cimport cython
import numpy as np
cimport numpy as np
DTYPE = np.float64
ctypedef np.float64_t DTYPE_t
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cdef _process(np.ndarray[DTYPE_t, ndim=2] array):
cdef unsigned int rows = array.shape[0]
cdef unsigned int cols = array.shape[1]
cdef unsigned int row
cdef np.ndarray[DTYPE_t, ndim=2] out = np.zeros((rows, cols))
for row in range(0, rows):
out[row, :] = np.sum(array - array[row, :], axis=0)
return out
def main():
cdef np.ndarray[DTYPE_t, ndim=2] data
cdef np.ndarray[DTYPE_t, ndim=2] out
data = np.load('data.npy')
out = _process(data)
np.save('viacynpy.npy', out)
Версия 3 (создание модуля с помощью cython)
import cython
cimport cython
import numpy as np
cimport numpy as np
DTYPE = np.float64
ctypedef np.float64_t DTYPE_t
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
cdef _process(np.ndarray[DTYPE_t, ndim=2] array):
cdef unsigned int rows = array.shape[0]
cdef unsigned int cols = array.shape[1]
cdef unsigned int row
cdef np.ndarray[DTYPE_t, ndim=2] out = np.zeros((rows, cols))
for row in range(0, rows):
for col in range(0, cols):
for row2 in range(0, rows):
out[row, col] += array[row2, col] - array[row, col]
return out
def main():
cdef np.ndarray[DTYPE_t, ndim=2] data
cdef np.ndarray[DTYPE_t, ndim=2] out
data = np.load('data.npy')
out = _process(data)
np.save('vialoop.npy', out)
С матрицей 10000x10, сохраненной в data.npy, время:
$ python -m timeit -c "from version1 import main;main()"
10 loops, best of 3: 4.56 sec per loop
$ python -m timeit -c "from version2 import main;main()"
10 loops, best of 3: 4.57 sec per loop
$ python -m timeit -c "from version3 import main;main()"
10 loops, best of 3: 2.96 sec per loop
Ожидается ли это или есть оптимизация, которую мне не хватает? Тот факт, что версии 1 и 2 дает тот же результат, как-то ожидалось, но почему версия 3 быстрее?
Ps.- Это НЕ расчет, который мне нужно сделать, просто простой пример, который показывает одно и то же.
Ответы
Ответ 1
Как упоминалось в других ответах, версия 2 по существу такая же, как и версия 1, поскольку cython не может выполнить поиск в операторе доступа к массиву, чтобы оптимизировать его. Для этого есть 2 причины
-
Во-первых, в каждом вызове функция numpy имеет определенный объем накладных расходов по сравнению с оптимизированным кодом C. Однако эти накладные расходы станут менее значительными, если каждая операция связана с большими массивами
-
Во-вторых, существует создание промежуточных массивов. Это более понятно, если вы рассматриваете более сложную операцию, например out[row, :] = A[row, :] + B[row, :]*C[row, :]
. В этом случае в память должен быть создан целый массив B*C
, а затем добавлен в A
Это означает, что кэширование ЦП прерывается, поскольку данные считываются и записываются в память, а не сохраняются в ЦП и используются сразу. Важно отметить, что эта проблема ухудшается, если вы имеете дело с большими массивами.
В частности, поскольку вы заявляете, что ваш реальный код более сложный, чем ваш пример, и он показывает гораздо большее ускорение, я подозреваю, что вторая причина, вероятно, будет основным фактором в вашем случае.
В стороне, если ваши вычисления достаточно просты, вы можете преодолеть этот эффект, используя numexpr, хотя, конечно, cython полезен во многих других ситуациях, поэтому это может быть лучший подход для вас.
Ответ 2
С небольшой модификацией версия 3 становится в два раза быстрее:
@cython.boundscheck(False)
@cython.wraparound(False)
@cython.nonecheck(False)
def process2(np.ndarray[DTYPE_t, ndim=2] array):
cdef unsigned int rows = array.shape[0]
cdef unsigned int cols = array.shape[1]
cdef unsigned int row, col, row2
cdef np.ndarray[DTYPE_t, ndim=2] out = np.empty((rows, cols))
for row in range(rows):
for row2 in range(rows):
for col in range(cols):
out[row, col] += array[row2, col] - array[row, col]
return out
Узким местом в вашем расчете является доступ к памяти. Ваш входной массив C упорядочен, что означает, что движение по последней оси делает наименьший скачок в памяти. Поэтому ваш внутренний цикл должен быть вдоль оси 1, а не оси 0. При этом это изменение сокращает время выполнения в два раза.
Если вам нужно использовать эту функцию на небольших входных массивах, вы можете уменьшить накладные расходы, используя np.empty
вместо np.ones
. Чтобы уменьшить накладные расходы, используйте PyArray_EMPTY
из API-интерфейса numpy C.
Если вы используете эту функцию на очень больших входных массивах (2 ** 31), то целые числа, используемые для индексирования (и в функции range
), будут переполняться. Для безопасного использования:
cdef Py_ssize_t rows = array.shape[0]
cdef Py_ssize_t cols = array.shape[1]
cdef Py_ssize_t row, col, row2
вместо
cdef unsigned int rows = array.shape[0]
cdef unsigned int cols = array.shape[1]
cdef unsigned int row, col, row2
Timing:
In [2]: a = np.random.rand(10000, 10)
In [3]: timeit process(a)
1 loops, best of 3: 3.53 s per loop
In [4]: timeit process2(a)
1 loops, best of 3: 1.84 s per loop
где process
- ваша версия 3.
Ответ 3
Я бы порекомендовал использовать флаг -a, чтобы cython сгенерировал html файл, который показывает, что происходит в чистом c, и вызывает API-интерфейс python:
http://docs.cython.org/src/quickstart/cythonize.html
Версия 2 дает почти тот же результат, что и в версии 1, потому что весь тяжелый подъем выполняется с помощью API Python (через numpy), а cython ничего не делает для вас. Фактически на моей машине numpy построен против MKL, поэтому, когда я компилирую cython сгенерированный c-код с помощью gcc, версия 3 на самом деле немного медленнее, чем две другие.
Cython светит, когда вы выполняете манипуляцию массивом, которую numpy не может выполнять "векторизованным" способом, или когда вы делаете что-то интенсивное, что позволяет избежать создания большого временного массива. Я получил 115-кратное ускорение, используя cython vs numpy для моего собственного кода:
https://github.com/synapticarbors/pylangevin-integrator
Часть этого вызова вызывала каталог randomkit на уровне кода c вместо того, чтобы вызывать его через numpy.random
, но большая часть из них была cython, переводящая вычислительно интенсивную для циклов в чистую c без вызовов на python.
Ответ 4
Разница может быть связана с тем, что версии 1 и 2 выполняют вызов уровня на уровне Python для np.sum()
для каждой строки, а версия 3, вероятно, компилируется в жесткий, чистый цикл C.
Изучая разницу между версиями 2 и 3, источником C-генерации, полученным Cython, следует просвещать.
Ответ 5
Я предполагаю, что основные накладные расходы, которые вы сохраняете, - это созданные временные массивы. Вы создаете большой массив array - array[row, :]
, а затем уменьшаете его до меньшего массива с помощью sum
. Но создание этого большого временного массива не будет бесплатным, особенно если вам нужно выделить память.