Почему Cython медленнее, чем векторизация NumPy?

Рассмотрим следующий код Cython:

cimport cython
cimport numpy as np
import numpy as np

@cython.boundscheck(False)
@cython.wraparound(False)
def test_memoryview(double[:] a, double[:] b):
    cdef int i
    for i in range(a.shape[0]):
        a[i] += b[i]

@cython.boundscheck(False)
@cython.wraparound(False)
def test_numpy(np.ndarray[double, ndim=1] a, np.ndarray[double, ndim=1] b):
    cdef int i
    for i in range(a.shape[0]):
        a[i] += b[i]

def test_numpyvec(a, b):
    a += b

def gendata(nb=40000000):
    a = np.random.random(nb)
    b = np.random.random(nb)
    return a, b

Запуск его в интерпретаторе дает (после нескольких прогонов, чтобы разогреть кеш):

In [14]: %timeit -n 100 test_memoryview(a, b)
100 loops, best of 3: 148 ms per loop

In [15]: %timeit -n 100 test_numpy(a, b)
100 loops, best of 3: 159 ms per loop

In [16]: %timeit -n 100 test_numpyvec(a, b)
100 loops, best of 3: 124 ms per loop

# See answer below :
In [17]: %timeit -n 100 test_raw_pointers(a, b)
100 loops, best of 3: 129 ms per loop

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

Я забыл оптимизацию в моем Cython-коде? Использует ли NumPy что-то (BLAS?), Чтобы сделать такие простые операции быстрее? Могу ли я повысить производительность этого кода?

Обновление:. Необработанная версия указателя находится на одном уровне с NumPy. Таким образом, очевидно, что некоторые издержки при использовании памяти или индексирования NumPy.

Ответ 1

Другой вариант - использовать необработанные указатели (и глобальные директивы, чтобы избежать повторения @cython...):

#cython: wraparound=False
#cython: boundscheck=False
#cython: nonecheck=False

#...

cdef ctest_raw_pointers(int n, double *a, double *b):
    cdef int i
    for i in range(n):
        a[i] += b[i]

def test_raw_pointers(np.ndarray[double, ndim=1] a, np.ndarray[double, ndim=1] b):
    ctest_raw_pointers(a.shape[0], &a[0], &b[0])

Ответ 2

На моей машине разница не такая большая, но я могу почти устранить ее, изменив функции просмотра numpy и памяти, подобные этой

@cython.boundscheck(False)
@cython.wraparound(False)
def test_memoryview(double[:] a, double[:] b):
    cdef int i, n=a.shape[0]
    for i in range(n):
        a[i] += b[i]

@cython.boundscheck(False)
@cython.wraparound(False)
def test_numpy(np.ndarray[double] a, np.ndarray[double] b):
    cdef int i, n=a.shape[0]
    for i in range(n):
        a[i] += b[i]

а затем, когда я компилирую вывод C из Cython, я использую флаги -O3 и -march=native. Это, по-видимому, указывает на то, что разница в таймингах исходит от использования различных оптимизаций компилятора.

Я использую 64-битную версию MinGW и NumPy 1.8.1. Ваши результаты, вероятно, будут зависеть от ваших версий пакетов, оборудования, платформы и компилятора.

Если вы используете макет Cython для тетради IPython, вы можете принудительно выполнить обновление с помощью дополнительных флагов компилятора, заменив %%cython на %%cython -f -c=-O3 -c=-march=native

Если вы используете стандартный setup.py для своего модуля cython, вы можете указать аргумент extra_compile_args при создании объекта Extension, который вы передаете на distutils.setup.

Примечание. Я удалил флаг ndim=1 при указании типов для массивов NumPy, потому что это необязательно. Это значение по умолчанию равно 1.

Ответ 3

Изменение, которое немного увеличивает скорость, это указать шаг:

def test_memoryview_inorder(double[::1] a, double[::1] b):
    cdef int i
    for i in range(a.shape[0]):
        a[i] += b[i]