Numpy против скорости Cython - PullRequest
       8

Numpy против скорости Cython

37 голосов
/ 18 октября 2011

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

Ответы [ 5 ]

41 голосов
/ 07 мая 2012

С небольшими изменениями версия 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.

34 голосов
/ 18 октября 2011

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

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

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

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

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

7 голосов
/ 18 октября 2011

Я бы порекомендовал использовать флаг -a, чтобы cython сгенерировал html-файл, который показывает, что переводится в чистый c против вызова API python:

http://docs.cython.org/src/quickstart/cythonize.html

Версия 2дает почти такой же результат, как и версия 1, потому что вся тяжелая работа выполняется API-интерфейсом Python (посредством numpy), а cython ничего не делает для вас.На самом деле на моей машине numpy построен на основе MKL, поэтому, когда я компилирую сгенерированный с помощью gthon код на cthon, версия 3 на самом деле немного медленнее двух других.

Cython светится, когда вы выполняете массивманипуляции, которые numpy не может выполнять «векторизованным» способом, или когда вы делаете что-то интенсивное, что позволяет избежать создания большого временного массива.Я получил ускорение в 115 раз, используя cython против numpy для моего собственного кода:

https://github.com/synapticarbors/pylangevin-integrator

Часть этого вызывала каталог randomkit на уровне кода c вместовызывая его через numpy.random, но большей частью это был перевод на Cython вычислительно интенсивных циклов for в чистый c без обращений к python.

3 голосов
/ 18 октября 2011

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

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

1 голос
/ 18 октября 2011

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

...