Добавление Cython в SciPy#

Как написано на Сайт Cython:

Cython — это оптимизирующий статический компилятор как для языка программирования Python, так и для расширенного языка программирования Cython (основанного на Pyrex). Он делает написание C расширений для Python таким же простым, как и сам Python.

Если ваш код в настоящее время выполняет много циклов в Python, он может получить выгоду от компиляции с помощью Cython. Этот документ предназначен для очень краткого введения: достаточно, чтобы увидеть, как использовать Cython с SciPy. Как только ваш код будет компилироваться, вы можете узнать больше о том, как оптимизировать его, ознакомившись с Документация Cython.

Есть только две вещи, которые нужно сделать, чтобы SciPy скомпилировал ваш код с помощью Cython:

  1. Включите свой код в файл с .pyx расширение, а не .py расширение. Все файлы с .pyx расширения автоматически преобразуются Cython в .c или .cpp файлы при сборке SciPy.

  2. Добавить новый .pyx файл в meson.build конфигурация сборки подпакета, в котором находится ваш код. Обычно уже есть другие .pyx присутствующие шаблоны (если нет, ищите в другом подмодуле), так что есть пример для следования тому, какой именно контент добавить в meson.build.

Пример#

scipy.optimize._linprog_rs.py содержит реализацию пересмотренного симплекс-метода для scipy.optimize.linprog. Пересмотренный симплекс-метод выполняет множество элементарных операций со строками матриц, и поэтому он был естественным кандидатом для Cython-оптимизации.

Обратите внимание, что scipy/optimize/_linprog_rs.py импортирует BGLU и LU классы из ._bglu_dense точно так же, как если бы они были обычными классами Python. Но это не так. BGLU и LU являются классами Cython, определёнными в /scipy/optimize/_bglu_dense.pyx. Ничего в способе их импорта или использования не указывает на то, что они написаны на Cython; единственный способ определить, что это классы Cython, заключается в том, что они определены в файле с расширением .pyx расширение.

Даже в /scipy/optimize/_bglu_dense.pyx, большая часть кода напоминает Python. Наиболее заметные различия — наличие cimport, cdef, и Cython декораторы. Ни один из них не является строго необходимым. Без них чистый Python-код все еще может быть скомпилирован Cython. Расширения языка Cython — это *просто* настройки для улучшения производительности. .pyx файл автоматически преобразуется в .c файл Cython при сборке SciPy.

Осталось только добавить конфигурацию сборки, которая будет выглядеть примерно так:

_bglu_dense_c = opt_gen.process('_bglu_dense.pyx')

py3.extension_module('_bglu_dense',
  _bglu_dense_c,
  c_args: cython_c_args,
  dependencies: np_dep,
  link_args: version_link_args,
  install: true,
  subdir: 'scipy/optimize'
)

При сборке SciPy, _bglu_dense.pyx будет транспилирован cython в код C, а затем этот сгенерированный C-файл обрабатывается Meson как любой другой C-код в SciPy - создавая модуль расширения, который мы сможем импортировать и использовать LU и BGLU классы из.

Упражнение#

Посмотрите видео с пошаговым выполнением этого упражнения: Cython-изация кода SciPy

  1. Обновить Cython и создать новую ветку (например, git checkout -b cython_test) для внесения некоторых экспериментальных изменений в SciPy

  2. Добавить простой код на Python в .py файл в /scipy/optimize директория, например /scipy/optimize/mypython.py. Например:

    def myfun():
        i = 1
        while i < 10000000:
            i += 1
        return i
    
  3. Давайте посмотрим, сколько времени занимает этот чистый Python-цикл, чтобы сравнить производительность Cython. Например, в IPython-консоли в Spyder:

    from scipy.optimize.mypython import myfun
    %timeit myfun()
    

    Я получаю что-то вроде:

    715 ms ± 10.7 ms per loop
    
  4. Сохраните ваш .py файл в .pyx файл, например mycython.pyx.

  5. Добавьте .pyx to scipy/optimize/meson.build, способом, описанным в предыдущем разделе.

  6. Пересоберите SciPy. Обратите внимание, что модуль расширения ( .so или .pyd файл) был добавлен в build/scipy/optimize/ каталог.

  7. Измерить время, например, перейдя в IPython с python dev.py ipython и затем:

    from scipy.optimize.mycython import myfun
    %timeit myfun()
    

    Я получаю что-то вроде:

    359 ms ± 6.98 ms per loop
    

    Cython ускорил чистый Python-код примерно в 2 раза.

  8. Это незначительное улучшение в общей схеме. Чтобы понять почему, полезно, чтобы Cython создал "аннотированную" версию нашего кода для отображения узких мест. В окне терминала вызовите Cython на вашем .pyx файл с -a флаг:

    cython -a scipy/optimize/mycython.pyx
    

    Обратите внимание, что это создает новый .html файл в /scipy/optimize каталог. Откройте .html файл в любом браузере.

  9. Желто-подсвеченные строки в файле указывают на потенциальное взаимодействие между скомпилированным кодом и Python, что значительно замедляет работу. Интенсивность подсветки указывает на предполагаемую серьезность взаимодействия. В данном случае, большая часть взаимодействия может быть устранена, если мы определим переменную i как целое число, чтобы Cython не рассматривал возможность того, что это общий объект Python:

    def myfun():
        cdef int i = 1  # our first line of Cython code
        while i < 10000000:
            i += 1
        return i
    

    Воссоздание аннотированного .html файл показывает, что большая часть взаимодействия с Python исчезла.

  10. Пересоберите SciPy, откройте новый консоль IPython и %timeit:

from scipy.optimize.mycython import myfun
%timeit myfun()

Я получаю что-то вроде: 68.6 ns ± 1.95 ns per loop. Код на Cython работал примерно в 10 миллионов раз быстрее, чем исходный код на Python.

В этом случае компилятор, вероятно, оптимизировал цикл, просто возвращая конечный результат. Такое ускорение не типично для реального кода, но это упражнение определённо иллюстрирует мощь Cython, когда альтернативой являются многие низкоуровневые операции в Python.