Повысьте производительность кода Python с помощью Cython

Краткое введение

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

Cython — это язык программирования, который делает написание расширений C для языка Python таким же простым, как и сам Python. Он стремится стать надстройкой языка Python, что дает ему высокоуровневое, объектно-ориентированное, функциональное и динамическое программирование. Помимо этого, его главной особенностью является поддержка необязательных объявлений статических типов как части языка. Исходный код транслируется в оптимизированный код C/C++ и компилируется в виде модулей расширения Python. Это обеспечивает как очень быстрое выполнение программы, так и тесную интеграцию с внешними библиотеками C, сохраняя при этом высокую производительность программиста, которой хорошо известен язык Python. Cython — это язык программирования, который делает написание расширений C для языка Python таким же простым, как Python. сам.

Во-первых, мы видим, что основная цель Cython – оптимизировать код Python, повышая его производительность.

Каким образом? Ну, переводя код Python в код C/C++, а затем компилируя его в модули расширения Python. Следующая диаграмма из Учебника Курта Смита по Cython (который я настоятельно рекомендую посмотреть) действительно поучительна.

Следуя диаграмме, мы начнем с создания файла .pyx, содержащего наш код Cython (в данном случае fib.pyx, который вычисляет числа Фибоначчи).

Затем Cython будет отвечать за создание файла .cfile (мы поговорим об этом подробнее в следующих разделах). Наконец, Cython скомпилирует .c file в .so file (или .pyd в Windows).

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

Как скомпилировать наш код Cython?

После прочтения раздела выше у вас должно быть общее представление о цели и основных функциях Cython. Но, предположим, мой код находится в .pyx , как мне указать Cython скомпилировать его? Просто, Cython предлагает нам три способа.

Старый добрый setup.py

Используйте метод cythonize, чтобы сообщить Cython, какие файлы нужно перевести и скомпилировать. Например, в приведенном выше фрагменте кода мы говорим Cython скомпилировать fib.pyx . После того, как мы настроили setup.py, просто выполните следующую команду в терминале.

python setup.py build_ext -if

Если все пойдет по плану, вы должны увидеть файлы .c и .so (или .pyd в Windows). После этого ваша cythonized функция (в данном случае fib ) будет доступна из вашего кода Python, поэтому мы сможем импортировать ее, как в чистом Python.

пиксимпорт

Если вы не хотите создавать файл setup.py (может быть, вы хотите сделать несколько быстрых проверок или просто поэкспериментировать), вы можете использовать pyximport , что позволяет нам импортировать функции Cython, как если бы это была обычная функция Python.

магия %%cython

Этот метод мой любимый, и я всегда использую его для отладки. Создайте/откройте блокнот Jupyter, импортируйте расширение Cython и используйте волшебную команду %%cython.

Весь код, определенный внутри ячейки с помощью волшебной команды, будет скомпилирован в фоновом режиме.

Печатать, печатать и еще раз печатать

В собственной документации Cython этот язык определяется как Python с типами C. И это именно то, что мы должны сделать, когда хотим перенести код Python на Cython… печатать, печатать и еще раз печатать.

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

Чтобы определить тип переменной, мы просто используем ключевое слово cdef. Например, в случае функции fib мы определили переменные i, a и b как целые числа.

Это значительно сокращает время вычислений, поскольку Python уже знает, что эти три переменные будут вести себя как целые числа во всем коде.

В дополнение к типизации переменных Cython также предлагает возможность типизации функций.

Снова используя cdef, мы можем создавать оптимизированные версии наших методов. Однако есть одна загвоздка, заключающаяся в том, что эти типы функций недоступны из Python, поэтому большую часть времени мы будем использовать их. как помощники для основной функции.

Не паникуйте, если это кажется сложным, потому что на практическом примере из последнего раздела вы увидите, что оптимизация функций относительно проста и довольно затягивает!

Как бороться с numpy.arrays?

Если вы занимаетесь большими данными / наукой о данных / машинным обучением / искусственным интеллектом /… почти невозможно, чтобы вы не знали NumPy. Более того, почти невозможно, чтобы вы им не пользовались, и почти невозможно, чтобы вам не было интересно, Cython и Numpy хорошо работают вместе.

Что ж, я рад сообщить вам, что в Cython можно работать с numpy.arrays;в частности, путем эффективного доступа к буферам памяти с помощью типизированных представлений памяти.

В качестве примера типизации массива рассмотрим следующие фрагменты кода. В первом примере a было numpy.array в исходном коде Python, поэтому мы должны сообщить Cython, что будем работать с типизированным представлением памяти. Даже если это звучит сложно, синтаксис довольно прост.

Итак, в данном случае мы сообщаем Cython, что arr — это двумерное представление памяти с элементами типа double. Если мы хотим работать с 3D-массивом, мы будем использовать тип double[:, :, :], с 4D-массивом — double[:, :, :, :] и т. д.

Однако есть более эффективный способ типизации, а именно указание порядка расположения элементов в массиве; непрерывно или несмежно в памяти.

Массив является непрерывным, если он хранится в непрерывном блоке памяти.

Давайте объясним это более подробно, используя впечатляющее объяснение пользователя Alex Riley в этом ответе StackOverflow.

Итак, предположим, что мы имеем дело с двумерным массивом (матрицей), например:

В памяти компьютера его значения хранятся так:

Это означает, что каждая строка хранится в блоке памяти. Массивы такого типа называются C-непрерывными.

Теперь предположим, что мы транспонируем исходную матрицу. В этом случае непрерывность теряется, потому что записи соседних строк больше не находятся в соседних адресах памяти. Однако у нас есть новый тип непрерывности столбцов, то есть столбцы находятся в смежных блоках памяти. Массивы такого типа называются Fortran-contiguous.

Но куда мы движемся со всей этой низкоуровневой теорией? 🤔🤔🤔

Дело в том, что сообщение Cython о том, с каким типом массивов мы имеем дело, может значительно улучшить наше время и выделение памяти!!

Если вы хотите узнать больше об использовании numpy.arrays в Cython, я рекомендую вам раздел официальной документации Cython для пользователей NumPy.

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

Практический пример

В этом практическом примере мы будем «китонизировать» известный алгоритм линейной алгебры, т. е. решать систему линейных уравнений с использованием метода Гаусса.

Мы будем следить за этой блокнотом, который я загрузил в свой репозиторий. Существуют более продвинутые алгоритмы линейной алгебры, если вы хотите взглянуть.

Решая эту тривиальную систему линейных уравнений, мы получаем время 8,81 мкс. Вы можете подумать, что сейчас хорошие времена, но мы можем добиться большего.

Теперь давайте применим все, что мы узнали из этого руководства, к нашему коду Python. Результат примерно такой.

Посмотрите на время сейчас:

962 нс!! Вот что я называю улучшением!!

Полезные ресурсы

Если этот урок был вам интересен, следите за мной в Twitter и Linkedin, где я также публикую похожий контент 😀😀😀

Linkedin → https://www.linkedin.com/in/moteropedrido/

Твиттер → https://twitter.com/MTrofficus