Почему Cython намного медленнее, чем Numba, когда перебирает массивы NumPy? - PullRequest
0 голосов
/ 06 ноября 2018

При переборе массивов NumPy Numba выглядит значительно быстрее, чем Cython.
Какие оптимизации Cython я могу пропустить?

Вот простой пример:

Чистый код Python:

import numpy as np

def f(arr):
  res=np.zeros(len(arr))

  for i in range(len(arr)):
     res[i]=(arr[i])**2

  return res

arr=np.random.rand(10000)
%timeit f(arr)

выход: 4,81 мс ± 72,2 мкс на цикл (среднее ± стандартное отклонение из 7 циклов, по 100 циклов в каждом)


Код Cython (внутри Jupyter):

%load_ext cython
%%cython

import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport pow

#@cython.boundscheck(False)
#@cython.wraparound(False)

cpdef f(double[:] arr):
   cdef np.ndarray[dtype=np.double_t, ndim=1] res
   res=np.zeros(len(arr),dtype=np.double)
   cdef double[:] res_view=res
   cdef int i

   for i in range(len(arr)):
      res_view[i]=pow(arr[i],2)

   return res

arr=np.random.rand(10000)
%timeit f(arr)

Out: 445 мкс ± 5,49 мкс на цикл (среднее ± стандартное отклонение из 7 циклов, 1000 циклов в каждом)


Код Numba:

import numpy as np
import numba as nb

@nb.jit(nb.float64[:](nb.float64[:]))
def   f(arr):
   res=np.zeros(len(arr))

   for i in range(len(arr)):
       res[i]=(arr[i])**2

   return res

arr=np.random.rand(10000)
%timeit f(arr)

Out: 9,59 мкс ± 98,8 нс на цикл (среднее ± стандартное отклонение из 7 циклов, 100000 циклов в каждом)


В этом примере Numba почти в 50 раз быстрее, чем Cython.
Будучи новичком на Cython, я думаю, что мне чего-то не хватает.

Конечно, в этом простом случае использование векторизованной функции NumPy square было бы гораздо более подходящим:

%timeit np.square(arr)

Out: 5,75 мкс ± 78,9 нс на цикл (среднее ± стандартное отклонение из 7 циклов, 100000 циклов каждый)

1 Ответ

0 голосов
/ 06 ноября 2018

Как отметил @Antonio, использование pow для простого умножения не очень разумно и приводит к значительным накладным расходам:

Таким образом, замена pow(arr[i], 2) на arr[i]*arr[i] приводит к довольно большому ускорению:

cython-pow-version        356 µs
numba-version              11 µs
cython-mult-version        14 µs

Оставшееся различие, вероятно, связано с разницей между компиляторами и уровнями оптимизации (в моем случае, llvm против MSVC). Возможно, вы захотите использовать clang для соответствия производительности numba (см., Например, SO-answer )

Чтобы упростить оптимизацию для компилятора, вы должны объявить входные данные как непрерывный массив, то есть double[::1] arr (см. этот вопрос почему это важно для векторизации), используйте @cython.boundscheck(False) ( используйте опцию -a, чтобы увидеть, что там меньше желтого), а также добавьте флаги компилятора (например, -O3, -march=native или аналогичные в зависимости от вашего компилятора, чтобы включить векторизацию, обратите внимание на флаги сборки, используемые по умолчанию, которые могут запрещать некоторая оптимизация, например -fwrapv ). В конце вы можете написать рабочий цикл в C, скомпилировать с правильной комбинацией flags / compiler и использовать Cython, чтобы обернуть его.

Кстати, введя параметры функции как nb.float64[:](nb.float64[:]), вы уменьшите производительность numba - больше нельзя допускать, что входной массив является непрерывным, что исключает векторизацию. Пусть numba обнаружит типы (или определит его как непрерывный, т. Е. nb.float64[::1](nb.float64[::1]), и вы получите лучшую производительность:

@nb.jit(nopython=True)
def nb_vec_f(arr):
   res=np.zeros(len(arr))

   for i in range(len(arr)):
       res[i]=(arr[i])**2

   return res

Приводит к следующему улучшению:

%timeit f(arr)  # numba version
# 11.4 µs ± 137 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit nb_vec_f(arr)
# 7.03 µs ± 48.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

И, как указывает @ max9111, нам не нужно инициализировать полученный массив с нулями, но мы можем использовать np.empty(...) вместо np.zeros(...) - эта версия даже превосходит numpy's np.square()

Характеристики различных подходов на моей машине:

numba+vectorization+empty     3µs
np.square                     4µs
numba+vectorization           7µs
numba missed vectorization   11µs
cython+mult                  14µs
cython+pow                  356µs
...