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

У меня есть код анализа, который выполняет некоторые тяжелые числовые операции с использованием numpy. Просто из любопытства попытался скомпилировать его с помощью cython с небольшими изменениями, а затем я переписал его, используя циклы для части numpy.

К моему удивлению, код, основанный на циклах, был намного быстрее (в 8 раз). Я не могу опубликовать полный код, но я собрал очень простое несвязанное вычисление, которое показывает похожее поведение (хотя разница во времени не такая большая):

Версия 1 (без цитона)

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


person Hernan    schedule 17.10.2011    source источник
comment
а почему версия 3 быстрее? Кажется риторическим. Вы расширили встроенную функцию, переписав ее. Вы сэкономили на накладных расходах. Что ты спрашиваешь?   -  person S.Lott    schedule 18.10.2011
comment
Этот код можно сделать намного быстрее, используя умножение матриц: out = (rows*eye((rows,cols))-ones((rows,cols))*data.   -  person cyborg    schedule 18.10.2011


Ответы (5)


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

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

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

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

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

person DaveP    schedule 18.10.2011
comment
Спасибо (всем) за ответы. Второй момент, кажется, проблема. Я профилировал вызов функций numpy в своем коде, и у меня нет больших накладных расходов, поскольку матрица довольно большая. Я посмотрю на numexpr - person Hernan; 18.10.2011
comment
Просто чтобы уточнить, numexpr должен дать вам производительность, аналогичную вашей версии 3. Он намного менее мощный, чем cython, поэтому, если у вас уже есть работающее решение cython, я бы придерживался его. - person DaveP; 18.10.2011
comment
Что касается второго пункта, как бы вы избежали перегрузки кеша ЦП? Будет ли разница, если вы сделаете prod = B[row, :] * C[row, :], а затем out[row, :] = A[row, :] + prod? - person Alex; 19.02.2014
comment
Алекс, по моему опыту, по какой-то причине это на самом деле медленнее. У меня была серия операций с массивами numpy, и, просто объединив их все в одну строку, я смог получить ускорение этого фрагмента кода на 10%. Я смог получить больше ускорения от numexpr, так как все задействованные промежуточные записи в память абсолютно убивают производительность. numexpr оптимизирует получаемый код, чтобы избежать выделения промежуточных массивов, поэтому он значительно экономит на операциях записи и промахах кэша. - person webb; 07.11.2015

С небольшой модификацией версия 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 из numpy C API.

Если вы используете эту функцию для очень больших входных массивов (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.

person kwgoodman    schedule 07.05.2012

Я бы рекомендовал использовать флаг -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 переводил циклы for, требующие больших вычислительных ресурсов, в чистый c без вызовов python.

person JoshAdel    schedule 18.10.2011

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

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

person Pi Delport    schedule 17.10.2011

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

person wisty    schedule 17.10.2011
comment
Основываясь на моих тестах, sum() имеет значение только тогда, когда массив относительно небольшой ‹100 элементов. Для большого массива >1000 элементов чистая сумма C-цикла() на самом деле не дает никакого преимущества. Потому что для большого массива накладные расходы на вызов функции sum()-python можно игнорировать. Для меня причудливая индексация NpyArray обычно приводит к огромным потерям скорости. - person Wang; 02.08.2012