Как оптимизировать эту функцию Cython?

У меня есть модуль Cython:

#!python
#cython: language_level=3, boundscheck=False, nonecheck=False

import numpy as np
cimport numpy as np

def portfolio_s2( double[:,:] cv, double[:] weights ):    
    """ Calculate portfolio variance"""
    cdef double s0
    cdef double s1
    cdef double s2
    s0 = 0.0
    for i in range( weights.shape[0] ):
        s0 += weights[i]*weights[i]*cv[i,i]

    s1 = 0.0
    for i in range( weights.shape[0]-1 ):
        s2 = 0.0
        for j in range( i+1, weights.shape[0] ):
            s2 += weights[j]*cv[i,j]
        s1+= weights[i]*s2
    return s0+2.0*s1 

У меня есть эквивалентная функция в Numba:

@nb.jit( nopython=True )
def portfolio_s2( cv, weights ):
    """ Calculate portfolio variance using numba """
    s0 = 0.0
    for i in range( weights.shape[0] ):
        s0 += weights[i]*weights[i]*cv[i,i]

    s1 = 0.0
    for i in range( weights.shape[0]-1 ):
        s2 = 0.0
        for j in range( i+1, weights.shape[0] ):
            s2 += weights[j]*cv[i,j]
        s1+= weights[i]*s2
    return s0+2.0*s1 

Для ковариационной матрицы размера 10 версия Numba работает в 20 раз быстрее, чем Cython. Я предполагаю, что это связано с чем-то, что я делаю неправильно в Cython, но я новичок в Cython и не знаю, что делать.

Использование оптимизации Cel...

Я написал скрипт для проверки кода Cel против версии Numba:

    sizes = [ 2, 3, 4, 6, 8, 12, 16, 32, 48, 64, 96, 128, 196, 256 ]
    cython_timings = []
    numba_timings = []
    for size in sizes:
        X = np.random.randn(100,size)
        cv = np.cov( X, rowvar=0 )
        w  = np.ones( cv.shape[0] )

        num_tests=10

        pm.portfolio_s2( cv, w )
        with Timer( 'Cython' ) as cython_timer:
            for _ in range( num_tests ):
                s2_cython = pm.portfolio_s2_opt( cv, w )
        cython_timings.append( cython_timer.interval )

        helpers.portfolio_s2( cv, w )
        with Timer( 'Numba' ) as numba_timer:
            for _ in range( num_tests ):
                s2_numba = helpers.portfolio_s2( cv, w )
        numba_timings.append( numba_timer.interval )

    plt.plot( sizes, cython_timings, label='Cython' )
    plt.plot( sizes, numba_timings, label='Numba' )
    plt.title( 'Execution Time By Covariance Size' )
    plt.legend()
    plt.show()

Результирующая диаграмма выглядит следующим образом:

введите здесь описание изображения

Диаграмма показывает, что для небольших ковариационных матриц Numba работает лучше. Но по мере увеличения размера ковариационной матрицы Cython лучше масштабируется и в конечном итоге превосходит его с большим отрывом.

Есть ли какие-то накладные расходы на вызов функций, из-за которых Cython имеет такую ​​​​плохую производительность для небольших матриц? Мой вариант использования этого кода будет включать вычисление ковариаций для множества небольших матриц ковариаций. Поэтому мне нужна лучшая производительность для маленьких матриц, а не для больших.


person Ginger    schedule 29.03.2015    source источник
comment
В документах по cython есть хорошее руководство. Также не забудьте объявить типы каждой отдельной переменной. Обратите внимание, что i не имеет статического типа.   -  person cel    schedule 29.03.2015
comment
если производительность cython плохая, то почему бы вам не придерживаться numba?   -  person jepio    schedule 30.03.2015
comment
Numba не позволяет мне создавать массивы в режиме nopython. Итак, я изучаю Cython.   -  person Ginger    schedule 30.03.2015
comment
Ваш тест может немного вводить в заблуждение: вы измеряете накладные расходы на вызов python-›cython (или python-›numba). Если вы 'cpdef' функцию и вызовете ее из Cython, это может быть лучше.   -  person DavidW    schedule 30.03.2015
comment
Являются ли накладные расходы на звонки значительными для вашей проблемы? Если это так, вы можете избежать вызова этой функции, добавив еще cythonizing кода и вообще не используя функцию. Обратите внимание, что вы обмениваете красивую структуру вашей программы на скорость. Я бы сделал это только в том случае, если вам действительно нужна эта дополнительная скорость.   -  person cel    schedule 30.03.2015


Ответы (1)


При использовании Cython важно убедиться, что все статически типизировано.

В вашем примере переменные цикла i и j не были введены. Объявление cdef size_t i, j уже дает вам огромное ускорение.

В разделе Working with NumPy документации cython есть хорошие примеры.

Это моя установка и оценка:

import numpy as np
n = 100
cv = np.random.rand(n,n)
weights= np.random.rand(n)

Оригинальная версия:

%timeit portfolio_s2(cv, weights)
10000 loops, best of 3: 147 µs per loop

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

%timeit portfolio_s2_opt(cv, weights)
100000 loops, best of 3: 10 µs per loop

А вот код:

import numpy as np
cimport numpy as np


def portfolio_s2_opt(double[:,:] cv, double[:] weights):    
    """ Calculate portfolio variance"""
    cdef double s0
    cdef double s1
    cdef double s2
    cdef size_t i, j

    s0 = 0.0
    for i in range( weights.shape[0] ):
        s0 += weights[i]*weights[i]*cv[i,i]

    s1 = 0.0
    for i in range( weights.shape[0]-1 ):
        s2 = 0.0
        for j in range( i+1, weights.shape[0] ):
            s2 += weights[j]*cv[i,j]
        s1+= weights[i]*s2
    return s0+2.0*s1 
person cel    schedule 29.03.2015
comment
Вероятно, стоит отметить, что большой выигрыш достигается при вводе i и j (Cython даже предупреждает меня, когда я этого не делаю). Использование интерфейса numpy, а не интерфейса double [:] memoryview, как в исходном коде, дает мне ускорение примерно на 2%. - person DavidW; 29.03.2015
comment
@DavidW, использование Memoryviews не дает мне ускорения. Однако сейчас им отдается предпочтение. Я собираюсь пересмотреть свой ответ. Спасибо что подметил это. - person cel; 29.03.2015