Встроенная функция Cython с массивом numpy в качестве параметра - PullRequest
17 голосов
/ 09 января 2011

Рассмотрим код, подобный следующему:

import numpy as np
cimport numpy as np

cdef inline inc(np.ndarray[np.int32_t] arr, int i):
    arr[i]+= 1

def test1(np.ndarray[np.int32_t] arr):
    cdef int i
    for i in xrange(len(arr)):
        inc(arr, i)

def test2(np.ndarray[np.int32_t] arr):
    cdef int i
    for i in xrange(len(arr)):
        arr[i] += 1

Я использовал ipython для измерения скорости test1 и test2:

In [7]: timeit ttt.test1(arr)
100 loops, best of 3: 6.13 ms per loop

In [8]: timeit ttt.test2(arr)
100000 loops, best of 3: 9.79 us per loop

Есть ли способ оптимизировать test1?Почему Cython не встроил эту функцию как сказано?

ОБНОВЛЕНИЕ: На самом деле мне нужен многомерный код, подобный этому:

# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False

import numpy as np
cimport numpy as np

cdef inline inc(np.ndarray[np.int32_t, ndim=2] arr, int i, int j):
    arr[i, j] += 1

def test1(np.ndarray[np.int32_t, ndim=2] arr):
    cdef int i,j    
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            inc(arr, i, j)


def test2(np.ndarray[np.int32_t, ndim=2] arr):    
    cdef int i,j    
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            arr[i,j] += 1   

Время для этого:

In [7]: timeit ttt.test1(arr)
1 loops, best of 3: 647 ms per loop

In [8]: timeit ttt.test2(arr)
100 loops, best of 3: 2.07 ms per loop

Явное встраивание дает 300xускорив.И моя настоящая функция довольно большая, поэтому встраивание делает обслуживание кода намного хуже

ОБНОВЛЕНИЕ2:

# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False

import numpy as np
cimport numpy as np

cdef inline inc(np.ndarray[np.float32_t, ndim=2] arr, int i, int j):
  arr[i, j]+= 1

def test1(np.ndarray[np.float32_t, ndim=2] arr):
    cdef int i,j    
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            inc(arr, i, j)


def test2(np.ndarray[np.float32_t, ndim=2] arr):    
    cdef int i,j    
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            arr[i,j] += 1    

cdef class FastPassingFloat2DArray(object):
    cdef float* data
    cdef int stride0, stride1 
    def __init__(self, np.ndarray[np.float32_t, ndim=2] arr):
        self.data = <float*>arr.data
        self.stride0 = arr.strides[0]/arr.dtype.itemsize
        self.stride1 = arr.strides[1]/arr.dtype.itemsize
    def __getitem__(self, tuple tp):
        cdef int i, j
        cdef float *pr, r
        i, j = tp        
        pr = (self.data + self.stride0*i + self.stride1*j)
        r = pr[0]
        return r
    def __setitem__(self, tuple tp, float value):
        cdef int i, j
        cdef float *pr, r
        i, j = tp        
        pr = (self.data + self.stride0*i + self.stride1*j)
        pr[0] = value        


cdef inline inc2(FastPassingFloat2DArray arr, int i, int j):
    arr[i, j]+= 1


def test3(np.ndarray[np.float32_t, ndim=2] arr):    
    cdef int i,j    
    cdef FastPassingFloat2DArray tmparr = FastPassingFloat2DArray(arr)
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            inc2(tmparr, i,j)

Время:

In [4]: timeit ttt.test1(arr)
1 loops, best of 3: 623 ms per loop

In [5]: timeit ttt.test2(arr)
100 loops, best of 3: 2.29 ms per loop

In [6]: timeit ttt.test3(arr)
1 loops, best of 3: 201 ms per loop

Ответы [ 3 ]

17 голосов
/ 06 июля 2014

Более 3 лет прошло с тех пор, как вопрос был опубликован, и за это время был достигнут значительный прогресс.По этому коду (Обновление 2 вопроса):

# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np

cdef inline inc(np.ndarray[np.int32_t, ndim=2] arr, int i, int j):
    arr[i, j]+= 1

def test1(np.ndarray[np.int32_t, ndim=2] arr):
    cdef int i,j    
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            inc(arr, i, j)

def test2(np.ndarray[np.int32_t, ndim=2] arr):    
    cdef int i,j    
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            arr[i,j] += 1

Я получаю следующие тайминги:

arr = np.zeros((1000,1000), dtype=np.int32)
%timeit test1(arr)
%timeit test2(arr)
   1 loops, best of 3: 354 ms per loop
1000 loops, best of 3: 1.02 ms per loop

Так что проблема воспроизводима даже спустя более 3 лет.Cython теперь имеет типизированных просмотров памяти , AFAIK он был введен в Cython 0.16, поэтому недоступен на момент публикации вопроса.С этим:

# cython: infer_types=True
# cython: boundscheck=False
# cython: wraparound=False
import numpy as np
cimport numpy as np

cdef inline inc(int[:, ::1] tmv, int i, int j):
    tmv[i, j]+= 1

def test3(np.ndarray[np.int32_t, ndim=2] arr):
    cdef int i,j
    cdef int[:, ::1] tmv = arr
    for i in xrange(tmv.shape[0]):
        for j in xrange(tmv.shape[1]):
            inc(tmv, i, j)

def test4(np.ndarray[np.int32_t, ndim=2] arr):    
    cdef int i,j
    cdef int[:, ::1] tmv = arr
    for i in xrange(tmv.shape[0]):
        for j in xrange(tmv.shape[1]):
            tmv[i,j] += 1

С этим я получаю:

arr = np.zeros((1000,1000), dtype=np.int32)
%timeit test3(arr)
%timeit test4(arr)
1000 loops, best of 3: 977 µs per loop
1000 loops, best of 3: 838 µs per loop

Мы почти там и уже быстрее, чем старомодный способ!Теперь функция inc() может быть объявлена ​​nogil, поэтому давайте объявим это так!Но упс:

Error compiling Cython file:
[...]

cdef inline inc(int[:, ::1] tmv, int i, int j) nogil:
    ^
[...]
Function with Python return type cannot be declared nogil

А-а-а, я полностью пропустил, что отсутствует тип возврата void!Еще раз, но теперь с void:

cdef inline void inc(int[:, ::1] tmv, int i, int j) nogil:
    tmv[i, j]+= 1

И, наконец, я получаю:

%timeit test3(arr)
%timeit test4(arr)
1000 loops, best of 3: 843 µs per loop
1000 loops, best of 3: 853 µs per loop

Так же быстро, как ручная установка!


Теперь просторади интереса я попытался Numba на этом коде:

import numpy as np
from numba import autojit, jit

@autojit
def inc(arr, i, j):
    arr[i, j] += 1

@autojit
def test5(arr):
    for i in xrange(arr.shape[0]):
        for j in xrange(arr.shape[1]):
            inc(arr, i, j)

Я получаю:

arr = np.zeros((1000,1000), dtype=np.int32)
%timeit test5(arr)
100 loops, best of 3: 4.03 ms per loop

Даже если он в 4,7 раза медленнее, чем Cython, скорее всего, потому чтоJIT-компилятору не удалось встроить inc(), я думаю, что это УДИВИТЕЛЬНО! Все, что мне нужно было сделать, это добавить @autojit и не нужно было путать код с неуклюжими объявлениями типов;88-кратное ускорение почти за ничто!

Я пробовал другие вещи с Numba, такие как

@jit('void(i4[:],i4,i4)')
def inc(arr, i, j):
    arr[i, j] += 1

или nopython=True, но не смог улучшить его.

Улучшение встраивания находится в списке разработчиков Numba , нам нужно только подать больше запросов, чтобы сделать его более приоритетным.;)

7 голосов
/ 20 января 2011

Проблема заключается в том, что присвоение массива numpy (или, что то же самое, передача его в качестве аргумента функции) - это не просто простое присваивание, а «извлечение буфера», которое заполняет структуру и извлекает информацию о шагах и указателяхлокальные переменные, необходимые для быстрой индексации.Если вы выполняете итерацию по умеренному количеству элементов, эти издержки O (1) легко амортизируются в цикле, но это, конечно, не относится к небольшим функциям.

Улучшение этого является высоким в списке пожеланий многих людей, но это нетривиальное изменение.См., Например, обсуждение в http://groups.google.com/group/cython-users/browse_thread/thread/8fc8686315d7f3fe

7 голосов
/ 10 января 2011

Вы передаете массив в inc() как объект Python типа numpy.ndarray. Передача объектов Python стоит дорого из-за таких проблем, как подсчет ссылок, и кажется, что предотвращает встраивание. Если вы передадите массив способом C, то есть в качестве указателя, test1() станет даже быстрее, чем test2() на моей машине:

cimport numpy as np

cdef inline inc(int* arr, int i):
    arr[i] += 1

def test1(np.ndarray[np.int32_t] arr):
    cdef int i
    for i in xrange(len(arr)):
        inc(<int*>arr.data, i)
...