Как сделать Cython намного быстрее, чем Python (без Numpy) для добавления двух массивов вместе? - PullRequest
2 голосов
/ 31 марта 2020

Я хочу использовать Cython, чтобы уменьшить время, необходимое для добавления двух массивов (поэлементно) без с использованием Numpy массивов. Базовый c Python подход, который я нашел самым быстрым, заключается в использовании списочного понимания следующим образом:

def add_arrays(a,b):
    return [m + n for m,n in zip(a,b)]

Мой подход к Cython немного сложнее и выглядит следующим образом:

from array import array
from libc.stdlib cimport malloc
from cython cimport boundscheck,wraparound

@boundscheck(False)
@wraparound(False)
cpdef add_arrays_Cython(int[:] Aarr, int[:] Barr):
    cdef size_t i, I
    I = Aarr.shape[0]
    cdef int *Carr = <int *> malloc(640000 * sizeof(int))
    for i in range(I):
        Carr[i] = Aarr[i]+Barr[i]
    result_as_array  = array('i',[e for e in Carr[:640000]])
    return result_as_array

Обратите внимание, что я использую @boundscheck(False) и @wraparound(False), чтобы сделать его еще быстрее. Кроме того, меня беспокоит очень большой массив (размер 640000), и я обнаружил, что он выходит из строя, если я просто использую cdef int Carr[640000], поэтому я использовал malloc(), что решило эту проблему. Наконец, я возвращаю структуру данных в виде Python массива типа integer.

Для профилирования кода я запустил следующее:

a = array.array('i', range(640000)) #create integer array
b = a[:] #array to add

T=time.clock()
for i in range(20): add_arrays(a,b) #Python list comprehension approach
print(time.clock() - T)

> 6,33 секунды

T=time.clock()
for i in range(20): add_arrays_Cython(a,b) #Cython approach
print(time.clock() - T)

> 4,54 секунды

Очевидно, что основанный на Cython подход дает ускорение примерно на 30%. Я ожидал, что ускорение будет ближе к порядку или даже больше (как это происходит для Numpy).

Что я могу сделать, чтобы ускорить код Cython дальше? Есть ли очевидные узкие места в моем коде? Я новичок в Cython, поэтому я могу что-то неправильно понять.

1 Ответ

1 голос
/ 31 марта 2020

Самым большим узким местом является преобразование указателя результата обратно в массив.

Вот оптимизированная версия:

from cython cimport boundscheck,wraparound
from cython cimport view

@boundscheck(False)
@wraparound(False)
cpdef add_arrays_Cython(int[:] Aarr, int[:] Barr):
    cdef size_t i, I
    I = Aarr.shape[0]
    result_as_array = view.array(shape=(I,), itemsize=sizeof(int), format='i')
    cdef int[:] Carr = result_as_array
    for i in range(I):
        Carr[i] = Aarr[i]+Barr[i]
    return result_as_array

Несколько вещей, на которые следует обратить внимание - вместо mallo c Используя временный буфер и затем копируя результат в массив, я создаю cython.view.array и преобразую его в int[:]. Это дает мне грубую скорость доступа к указателю, а также позволяет избежать ненужного копирования. Я также возвращаю объект Cython напрямую, без предварительного преобразования его в python объект. В целом, это дает мне 70-кратное ускорение по сравнению с вашей первоначальной реализацией Cython.

Преобразование объекта view в список оказалось сложным: если вы просто измените инструкцию return на return list(result_as_array), код стал примерно в 10 раз медленнее , чем ваша первоначальная реализация. Но если вы добавите дополнительный слой переноса следующим образом: return list(memoryview(result_as_array)) функция была примерно в 5 раз быстрее вашей версии. Опять же, основные накладные расходы переходили от быстрого собственного нативного объекта к универсальному c python, и этого всегда следует избегать, если вам нужен быстрый код.

Для сравнения я запустил код с numpy. Версия numpy работала так же быстро, как и моя версия Cython. Это означает, что компилятор C смог автоматически векторизовать парное суммирование l oop внутри моего кода.

Примечание: вам нужно вызвать free() для указателей malloc(), в противном случае вы теряете память.

...