Самый эффективный способ построить одномерный массив/список/вектор неизвестной длины с использованием Cython? Или этого никогда не следует делать?

У меня есть критическая по времени модель, которую я написал на Cython. Основная функция моего расширения Cython имеет один цикл, и, согласно профилировщику Cython (где он показывает количество вызовов Python оттенками желтого), единственная «желтая» часть в настоящее время находится там, где я добавляю к списку Python. (Мне нужно вывести объект Python, поскольку я вызываю свою функцию Cython в скрипте Python). Это основная идея моей функции (остальное лишнее, я проверил каждую часть этой функции, и операция добавления является узким местом):

from libc.math cimport log
def main(some args):
    cdef (some vars)

    cdef list OutputList = []

    # NB: all vars have declared types
    for x in range(t):
        (do some Cythonic stuff, some of which uses my cimport-ed log)
        if condition is True:
            OutputList.append(x) # this is the only 'yellow' line in my main loop.
    return OutputList # return Python object to Python script that calls main()

К сожалению, я не знаю длину моего выходного массива/списка/вектора (что бы я ни использовал). Тем не менее, я мог бы установить его на 52560, и в конечном итоге я изменил его размер, чтобы уменьшить строку в каком-то другом коде Python. Я хотел бы получить значительный прирост скорости, не устанавливая длину выходного массива, но я с радостью откажусь от этой надежды, если она меня сдерживает.

Я также пытался использовать С++ в Cython для использования структур данных С++ (вектор, очередь и т. д.), но это лишает меня возможности красиво цимпортировать журнал. Я вижу в документации/вики Cython, что вы можете написать модуль 'shim' для использования функций чистого C в C++ Cython, но я понятия не имею, как это сделать, и я не могу найти ничего о том, как это сделать.

В любом случае, я приветствую все предложения, которые придерживаются моего вопроса:

Как лучше всего построить список/массив/вектор неизвестного размера в Cython? Или есть явная альтернатива (например, решение с итерируемым объектом известной длины), которая делает мою проблему неизвестной длины спорной?

Обновить

Контейнеры C++ показали увеличение скорости по сравнению с назначением элементов, а назначение элементов действительно показало увеличение скорости по сравнению с добавлением в списки и массивы numpy. Лучшим методом было бы использование контейнеров C++, а также возможность cимпортировать функции чистого C... это предотвратило бы замедление работы из-за необходимости искать функцию быстрого журнала за пределами libc.math.


person mdscruggs    schedule 13.09.2011    source источник


Ответы (3)


Добавление списков Python — хорошо оптимизированная операция в CPython. Python не выделяет память для каждого элемента, а постепенно увеличивает массивы указателей на объекты в списке. Так что простое переключение на Cython здесь не очень поможет.

Вы можете использовать контейнеры С++ в Cython следующим образом:

from libc.math cimport log
from libcpp.list cimport list as cpplist

def main(int t):

    cdef cpplist[int] temp

    for x in range(t):
        if x> 0:
            temp.push_back(x)

    cdef int N = temp.size()
    cdef list OutputList = N*[0]

    for i in range(N):
        OutputList[i] = temp.front()
        temp.pop_front()

    return OutputList  

Вы должны проверить, ускорит ли это процесс, но, возможно, вы не сильно ускоритесь.

Другой способ - использовать массивы numpy. Здесь Cython очень хорош в оптимизации кода. Поэтому, если вы можете жить с массивом numpy в качестве возвращаемого значения main, вы должны это учитывать и заменить построение и заполнение OutputList некоторым кодом Cython, выделяющим и заполняющим массив numpy.

Для получения дополнительной информации см. http://docs.cython.org/src/tutorial/numpy.html

Спросите, нужна ли вам помощь.

ОБНОВЛЕНИЕ: код должен быть немного быстрее, если вы избегаете поиска метода в обоих циклах:

from libc.math cimport log
from libcpp.list cimport list as cpplist

def main(int t):

    cdef cpplist[int] temp

    push_back = temp.push_back
    for x in range(t):
        if x> 0:
            push_back(x)

    cdef int N = temp.size()
    cdef list OutputList = N*[0]

    front = temp.front()
    pop_front = temp.pop_front()
    for i in range(N):
        OutputList[i] = front()
        pop_front()

    return OutputList  
person rocksportrocker    schedule 13.09.2011
comment
В моем сообщении упоминалось, что я пробовал контейнеры C++. Я выиграл время на части построения вывода, но потерял больше времени, чем я получил, из-за проблемы Cython с использованием функций чистого C в режиме C++, что лишило меня возможности использовать журнал cimport-ed из libc.math. Я пытался использовать массивы numpy для построения вывода, но, возможно, мои методы неэффективны. Я должен повторно протестировать несколько методов для выделения массива numpy (например, я пробовал outlist = np.append(outlist, newvalue), и это казалось очень медленным, даже с объявленными типами и интерфейсом буфера). - person mdscruggs; 13.09.2011
comment
использование np.append должно быть медленным. Либо вы собираете свои данные в какой-либо контейнер, либо сначала считаете свои элементы, выделяете соответствующий массив numpy, а затем заполняете массив. Последний шаг должен быть очень быстрым. - person rocksportrocker; 13.09.2011
comment
@mdscruggs: пожалуйста, сообщите, какой метод дает вам наибольшее ускорение. - person rocksportrocker; 16.09.2011
comment
push_back = temp.push_back были бесполезны, даже если бы они скомпилировались. .push_back() не является виртуальным методом, адрес известен во время компиляции. - person jfs; 23.09.2011
comment
@JF: компилируется, но ты прав. Если вы используете контейнерные классы С++, в скомпилированном коде больше нет поиска методов. Ситуация отличается, если temp был объектом Python. В этом случае сгенерированный код будет выполнять поиск метода во время выполнения. - person rocksportrocker; 23.09.2011
comment
@rocksportrocker: gcc говорит argument of type ‘void (std::vector<int>::)(const std::vector<int, std::allocator<int> >::value_type&)’ does not match ‘void (*)(int&)’. - person jfs; 23.09.2011

build1darray.pyx:

#cython: boundscheck=False, wraparound=False
from libc.math cimport log

from cython.parallel cimport prange

import numpy as pynp
cimport numpy as np

# copy declarations from libcpp.vector to allow nogil
cdef extern from "<vector>" namespace "std":
    cdef cppclass vector[T]:
        void push_back(T&) nogil
        size_t size()
        T& operator[](size_t)

def makearray(int t):
    cdef vector[np.float_t] v
    cdef int i
    with nogil: 
        for i in range(t):
            if i % 10 == 0:
                v.push_back(log(i+1))

    cdef np.ndarray[np.float_t] a = pynp.empty(v.size(), dtype=pynp.float)
    for i in prange(a.shape[0], nogil=True):
        a[i] = v[i]
    return a

The 2nd part is ~1% of the first loop therefore it doesn't make sense to optimize it for speed in this case.

<math.h> имеет extern "C" { ... } в моей системе, поэтому libc.math.log работает.

PyArray_SimpleNewFromData() можно использовать, чтобы избежать копирования данных за счет самостоятельного управления памятью для массива.

person jfs    schedule 22.09.2011

Что вы можете сделать, так это подсчитать, сколько элементов соответствует вашим критериям, а затем выделить достаточно большой массив numpy для этих элементов.

# pseudo code
def main(): 
   count = 0
   for i in range(t):
       if criteria: 
            count += 1

   cdef numpy.ndarray[count] result

   int idx =0
   for i in range(t):
      if criteria:
          idx += 1
          result[idx] = value
person fabrizioM    schedule 13.09.2011
comment
К сожалению, я генерирую новые данные с помощью моделирования нелинейной модели, поэтому у меня нет возможности заранее узнать, какой длины будет выходной массив. - person mdscruggs; 13.09.2011