Скорость вращения шпинделя против Китона

У меня есть код анализа, который выполняет некоторые тяжелые числовые операции с использованием 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) 

С матрицей 10000×10, сохраненной в 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.- Это НЕ расчет, который мне нужно сделать, просто простой пример, который показывает то же самое.

5 Solutions collect form web for “Скорость вращения шпинделя против Китона”

Как упоминалось в других ответах, версия 2 по существу такая же, как и версия 1, поскольку cython не может выполнить поиск в операторе доступа к массиву, чтобы оптимизировать его. Для этого есть 2 причины

  • Во-первых, в каждом вызове функция numpy имеет определенный объем накладных расходов по сравнению с оптимизированным кодом C. Однако эти накладные расходы станут менее значительными, если каждая операция связана с большими массивами

  • Во-вторых, существует создание промежуточных массивов. Это более понятно, если вы рассматриваете более сложную операцию, например out[row, :] = A[row, :] + B[row, :]*C[row, :] . В этом случае в память должен быть создан целый массив B*C , а затем добавлен в A Это означает, что кэширование ЦП прерывается, поскольку данные считываются и записываются в память, а не сохраняются в ЦП и используются сразу. Важно отметить, что эта проблема ухудшается, если вы имеете дело с большими массивами.

В частности, поскольку вы заявляете, что ваш реальный код более сложный, чем ваш пример, и он показывает гораздо большее ускорение, я подозреваю, что вторая причина, вероятно, будет основным фактором в вашем случае.

В стороне, если ваши вычисления достаточно просты, вы можете преодолеть этот эффект, используя numexpr , хотя, конечно, cython полезен во многих других ситуациях, поэтому это может быть лучший подход для вас.

При незначительной модификации версия 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 

Сроки:

 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.

Я бы порекомендовал использовать флаг -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.

Разница может заключаться в том, что версии 1 и 2 выполняют вызов уровня np.sum() на np.sum() для каждой строки, в то время как версия 3, вероятно, компилируется в жесткий, чистый цикл C.

Изучение разницы между источником C-источника C версии 2 и 3 должно быть просвечивающим.

Я предполагаю, что основные накладные расходы, которые вы сохраняете, – это созданные временные массивы. Вы создаете массив больших массивов array - array[row, :] , а затем уменьшаете его до меньшего массива с использованием sum . Но создание этого большого временного массива не будет бесплатным, особенно если вам нужно выделить память.

  • Ajax v., Включая данные в HTML
  • Отличается ли производительность между Python или C ++-кодированием OpenCV?
  • Самый эффективный способ сделать заявление if-elif-elif-else, когда else сделан больше всего?
  • Самый эффективный способ определения периодов перекрытия в Python
  • Почему функция numpy einsum работает быстрее, чем встроенные функции numpy?
  • numpy на многоядерном оборудовании
  • Почему numpy медленнее, чем для цикла
  • Почему реализация JITted Python все еще медленная?
  • Пункт номер быстрее, чем оператор
  • Почему чтение нескольких файлов происходит в то же время медленнее, чем чтение последовательно?
  • Python - ускорить преобразование категориальной переменной в ее числовой индекс
  • Python - лучший язык программирования в мире.