2011-01-09 4 views
15

Рассмотрим такой код:Cython встроенная функция с Numpy массива в качестве параметра

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 не встраивает эту функцию, как сказано?

UPDATE: На самом деле то, что мне нужно многомерный следующий код:

# 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 

Timing для него:

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 ускорение. И моя реальная функция довольно большой, так что встраивание это делает код ремонтопригодность намного хуже

UPDATE2:

# 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 
+0

На моей машине разница в двух измерениях составляет около 5% (вместо 30000%). Какие версии Python и Cython вы используете? Какой компилятор C? –

+0

Windows, Python 2.6, Cython 0.14, Gcc 4.5.1. Не могли бы вы разместить свой 2d-код? – Maxim

+0

Теперь я вижу разницу: я просто добавил 'ndim = 2' к первой версии вашего кода (потому что я думал, что это то, чего вы хотели получить). Если 'inc()' должен действовать только на одно целое число, просто передайте указатель на это единственное целое число 'inc()' - что-то вроде ' (arr.data + i * arr.strides [0] + j * arr.strides [1]) '. –

ответ

6

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

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

+0

Да, это был мой вопрос. Я просто приму свой ответ. – Maxim

7

Вы передаете массив в inc() как Объект Python типа numpy.ndarray. Передача объектов Python является дорогостоящей из-за проблем, таких как подсчет ссылок, и, похоже, предотвращает inlining. Если вы передать массив путь 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) 
+0

Хорошо, а как насчет 2-го и 3-го массивов? – Maxim

+0

@Maxim: ваш собственный код работает только для одномерных массивов, поэтому я предоставил более быструю версию только для этого случая. (Обратите внимание, что 'ndim = 1' неявно, если вы не указали явный параметр' ndim' для 'ndarray'.) Когда я добавляю 'ndim = 2' к вашему коду и времени' test1() 'и' test2() 'с массивом 50x50, между ними практически нет разницы в производительности на машине. –

+1

Пожалуйста, уточните в вопросе. Здесь я также получаю огромную разницу в производительности на ndim = 2 (что ожидается, потому что если inc не встроен, он приобретает и освобождает буфер numpy при каждом вызове). И пропустить только указатель в nD случае недостаточно, потому что вам также нужно знать размеры в каждом измерении, и передача их всех делает функцию плохой, и делает каждый доступ к массиву сложным .... – Maxim

12

Прошло более 3 лет с момента публикации вопроса, и за это время был достигнут значительный прогресс. В этом коде (Update 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 теперь имеет typed memoryviews, 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.7x медленнее, чем Cython, скорее всего потому, JIT компилятор не смог подключиться inc(), я думаю, это УДИВИТЕЛЬНО! Все, что мне нужно было сделать, это добавить @autojit и не нужно испортить код с неуклюжими объявлениями типа; 88-кратное ускорение почти ничего!

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

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

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

Improving inlining is on the Numba developers' list, нам нужно подать больше запросов, чтобы иметь более высокий приоритет. ;)