Cython: более чем 30-кратное ускорение Python-кода
Python — это язык, который любят многие программисты. Этим языком невероятно легко пользоваться. Всё дело в том, что код, написанный на Python, отличается интуитивной понятностью и хорошей читабельностью. Однако в разговорах о Python часто можно слышать одну и ту же жалобу на этот язык. Особенно тогда, когда о Python говорят знатоки языка C. Вот как она звучит: «Python — это медленно». И те, кто так говорят, не грешат против истины.
В сравнении со многими другими языками программирования Python — это, и правда, медленно. Вот результаты испытаний, в ходе которых сопоставляется производительность разных языков программирования при решении различных задач.
Есть несколько способов ускорения Python-программ. Например, можно применять библиотеки, рассчитанные на использование нескольких ядер процессора. Тем, кто работает с Numpy, Pandas или Scikit-Learn, можно посоветовать взглянуть на программный комплекс Rapids, позволяющий задействовать GPU при проведении научных расчётов.
Все эти методики ускорения работы хороши в тех случаях, когда решаемые с помощью Python задачи могут быть распараллелены. Например — это задачи по предварительной обработке данных или операции с матрицами.
Но как быть в том случае, если ваш код — это чистый Python? Что если у вас есть большой цикл for , который вам совершенно необходимо использовать, и выполнение которого просто нельзя распараллелить из-за того, что обрабатываемые в нём данные должны обрабатываться последовательно? Можно ли как-то ускорить сам Python?
Ответ на этот вопрос даёт Cython — проект, используя который можно значительно ускорить код, написанный на Python.
Что такое Cython?
Cython, по своей сути, это промежуточный слой между Python и C/C++. Cython позволяет писать обычный Python-код с некоторыми незначительными модификациями, который затем напрямую транслируется в C-код.
Единственное изменение Python-кода при этом заключается в добавлении к каждой переменной информации об её типе. При написании обычного кода на Python переменную можно объявить так:
При использовании Cython при объявлении переменной нужно указать её тип:
Эта конструкция сообщает Cython о том, что переменная представляет собой число с плавающей точкой. По такому же принципу объявляют переменные и в C. При использовании обычного Python типы переменных определяются динамически. Явное объявление типов, применяемое в Cython — это то, что делает возможным преобразование Python-кода в C-код. Дело в том, что в C необходимо явное объявление типов переменных.
Установка Cython предельно проста:
Типы в Cython
При использовании Cython можно выделить два набора типов. Один — для переменных, второй — для функций.
Если речь идёт о переменных, то тут нам доступны следующие типы:
- cdef int a, b, c
- cdef char *s
- cdef float x = 0.5 (число одинарной точности)
- cdef double x = 63.4 (число двойной точности)
- cdef list names
- cdef dict goals_for_each_play
- cdef object card_deck
При работе с функциями нам доступны следующие типы:
- def — обычная Python-функция, вызывается только из Python.
- cdef — Cython-функция, которую нельзя вызвать из обычного Python-кода. Такие функции можно вызывать только в пределах Cython-кода.
- cpdef — Функция, доступ к которой можно получить и из C, и из Python.
Ускорение кода с использованием Cython
Начнём с создания Python-бенчмарка. Это будет цикл for , в котором выполняется вычисление факториала числа. Соответствующий код на чистом Python будет выглядеть так:
def test(x): y = 1 for i in range(1, x+1): y *= i return y
Cython-эквивалент этой функции очень похож на её исходный вариант. Соответствующий код нужно поместить в файл с расширением .pyx . Единственное изменение, которое нужно внести в код, заключается в добавлении в него сведений о типах переменных и функции:
cpdef int test(int x): cdef int y = 1 cdef int i for i in range(1, x+1): y *= i return y
Обратите внимание на то, что перед функцией стоит ключевое слово cpdef . Это позволяет вызывать данную функцию из Python. Кроме того, тип назначен и переменной i , играющей роль счётчика цикла. Не будем забывать о том, что типизировать нужно все переменные, объявленные в функции. Это позволит компилятору C узнать о том, какие именно типы ему использовать.
Теперь создадим файл setup.py , который поможет нам преобразовать Cython-код в C-код:
from distutils.core import setup from Cython.Build import cythonize setup(ext_modules = cythonize('run_cython.pyx'))
python setup.py build_ext --inplace
Теперь С-код готов к использованию.
Если взглянуть в папку, в которой находится Cython-код, там можно будет найти все файлы, необходимые для запуска C-кода, включая файл run_cython.c . Если вам интересно — откройте этот файл и посмотрите на то, какой С-код сгенерировал Cython.
Теперь всё готово к тестированию нашего сверхбыстрого C-кода. Ниже приведён код, используемый для тестирования и сравнения двух вариантов программы.
import run_python import run_cython import time number = 10 start = time.time() run_python.test(number) end = time.time() py_time = end - start print("Python time = <>".format(py_time)) start = time.time() run_cython.test(number) end = time.time() cy_time = end - start print("Cython time = <>".format(cy_time)) print("Speedup = <>".format(py_time / cy_time))
Код этот устроен очень просто. Мы импортируем необходимые файлы — так же, как импортируются обычные Python-файлы, после чего вызываем соответствующие функции, делая это так же, как если бы мы всё время работали бы с обычными Python-функциями.
Взгляните на следующую таблицу. Можно заметить, что Cython-версия программы оказывается быстрей её Python-версии во всех случаях. Чем масштабнее задача — тем больше и ускорение, которое даёт использование Cython.
Число | Показатель Python Time | Показатель Cython Time | Показатель Speedup |
10 | 1.6689300537109375e-06 | 4.76837158203125e-07 | 3.5 |
100 | 3.337860107421875e-06 | 4.76837158203125e-07 | 7.0 |
1000 | 2.193450927734375e-05 | 9.5367431640625e-07 | 23.0 |
10000 | 0.0002090930938720703 | 6.4373016357421875e-06 | 32.481 |
100000 | 0.0021562576293945312 | 6.008148193359375e-05 | 35.89 |
1000000 | 0.02128767967224121 | 0.0005953311920166016 | 35.75 |
10000000 | 0.2148280143737793 | 0.00594782829284668 | 36.1187317112278 |
Итоги
Использование Cython позволяет значительно ускорить практически любой код, написанный на Python, не прилагая к этому особенных усилий. Чем больше в программе циклов и чем больше данных она обрабатывает — тем лучших результатов можно ждать от применения Cython.
Уважаемые читатели! Используете ли вы Cython в своих проектах?
Вышел Cython 3.0
17 июля 2023 года спустя пять лет разработки состоялся релиз компилятора для языков Python и Cython — проекта Cython 3.0.
Компилятор транслирует код на Python в представление на языках C/C++ и предоставляет дополнительные возможности для взаимодействия кода на языках Python и C. Cython наиболее востребован для создания Python-обвязок вокруг библиотек на языке C и для создания модулей на C, ускоряющих выполнение кода на языке Python.
Исходный код компилятора Cython 3.0 выложен на GitHub и распространяется под открытой лицензией Apache 2.0.
Согласно данным портала OpenNET, язык программирования Cython является расширенным вариантом языка Python, нацеленным на упрощение интеграции с кодом на языке C и позволяющим создавать расширения на языке C для проектов на языке Python так же просто, как писать код на Python.
Cython расширяет возможности Python средствами для прямого вызова функций на языке С, поддерживает определение переменных с типами языка С и позволяет компилировать итоговый код на языке Cython в представление на языке С, которое затем собирается штатным системным компилятором. Применение расширенных возможностей языка Cython позволяет значительно повысить эффективность выдаваемого компилятором кода.
Производительность выполнения Python-кода при использовании Cython примерно на 30% выше, чем при использовании CPython при тестировании пакетом pybench.
В Cython 3.0 реализована поддержка большинства возможностей ветки Python 3.11. Также в Cython 3.0 добавлена поддержка Unicode символов в идентификаторах, обеспечена автоматическая генерация ufunc для NumPy, добавлены быстрые расширенные типы @dataclass и @total_ordering, по умолчанию обеспечена безопасная обработка исключений в функциях на C, добавлена ограниченная поддержка API CPython.
Предыдущий релиз Cython имел номер 0.29. Присвоение номера версии 3.0 подчёркивает переход Cython на использование синтаксиса и семантики Python 3 по умолчанию. В Cython 0.29 по умолчанию применялась семантика Python 2.7.
Немного Сythonа
Дошли руки до Cythona, спасибо самоизоляции. Проблема прозаична — как ускориться на python с минимальными потерями в синтаксисе. Один из подходов — использование Сython (смесь С и python).
Не давала покоя публикация с громким названием. Но из содержания публикации мало что можно вынести, так как формулы и результирующая таблица неверны. Попробуем дополнить картину, начатую авторами поста и расставим точки над и.
*Тесты проводились на odroid xu4, ubuntu mate, python 2.7.17.
Cython ставится просто (pip install cython).
Будем мучить все те же числа Фибоначчи. Создадим файлы для тестов прироста производительности. Для языка python (test.py):
def test(n): a, b = 0.0, 1.0 for i in range(n): a, b = a + b, a print (a)
def test2(int n): cdef int i cdef double a=0.0, b=1.0 for i in range(n): a, b = a + b, a print (a)
Файл cython требует предварительной сборки. Для него создадим setup.py c содержимым:
from distutils.core import setup from Cython.Build import cythonize setup(ext_modules=cythonize('test2.pyx'))
python setup.py build_ext --inplace
Теперь возьмем файл из упомянутого поста с тестами и немного его поправим, добавив возможность вводить собственное число на старте (tests.py):
import test import test2 import time number = input('enter number: ') start = time.time() test.test(number) end = time.time() py_time = end - start print("Python time = <>".format(py_time)) start = time.time() test2.test(number) end = time.time() cy_time = end - start print("Cython time = <>".format(cy_time)) print("Speedup = <>".format(py_time / cy_time))
enter number: 10 Python time = 1.62124633789e-05 Cython time = 4.05311584473e-06 Speedup = 4.0 enter number: 100 Python time = 3.40938568115e-05 Cython time = 5.00679016113e-06 Speedup = 6.80952380952 enter number: 1000 Python time = 0.000224113464355 Cython time = 1.19209289551e-05 Speedup = 18.8 enter number: 100000 Python time = 0.0200171470642 Cython time = 0.000855922698975 Speedup = 23.3866295265
enter number: 10 Python time = 7.653236389160156e-05 Cython time = 2.8133392333984375e-05 Speedup = 2.7203389830508473 enter number: 100 Python time = 8.678436279296875e-05 Cython time = 3.170967102050781e-05 Speedup = 2.736842105263158 enter number: 1000 Python time = 0.00031876564025878906 Cython time = 4.673004150390625e-05 Speedup = 6.821428571428571 enter number: 100000 Python time = 0.01643967628479004 Cython time = 0.0004260540008544922 Speedup = 38.5858981533296
*модуль test2.pyx «пересобирался» командой:
python3 setup.py build_ext —inplace
**устанавливался cython:
pip3 install cython
Можно обойтись без сборки test2.pyx с использованием setup.py, для этого необходимо просто в файл tests.py добавить строки:
import pyximport pyximport.install()
Теперь test2.pyx будет собираться на лету при каждом запуске tests.py, а файлов в папке будет меньше.
Как завести cython на windows.
Несмотря на то, что cython допускает сборку файлов как под python3, так и python2, получить готовый рецепт под python3 не удалось удалось.
С python3 работает команда сборки:
python setup.py build_ext -i --compiler=msvc
Однако для ее полноценной работы необходима установка части компонентов visual studio 2019. Что именно устанавливать, указано в решении здесь.
Поэтому существует два рабочих варианта, которые позволяют поработать (собрать файл) в windows c cython.
Первый использует python2.7 и компилятор mingw.
Процедура следующая.
1.Устанавливаем сам cython под python2.7:
py -2 -m pip install cython
2.Устанавливаем компилятор mingw:
mingw
3.После установки компилятора и добавления его в PATH windows файл формата .pyx можно собирать командой:
python setup.py build_ext -i --compiler=mingw32
Второй использует python3.x и компилятор msvc.
Как запустить cython в jupyter notebook.
Иногда необходимо протестировать работу кода наглядно, используя jupyter. Чтобы каждый раз не компилировать код в cmd, можно использовать cython прямо в ячейках jupyter.
Для этого импортируем cython, выполнив в ячейке jupyter:
%%cython -a import numpy as np cdef int max(int a, int b): return a if a > b else b cdef int chebyshev(int x1, int y1, int x2, int y2): return max(abs(x1 - x2), abs(y1 - y2)) def c_benchmark(): a = np.random.rand(1000, 2) b = np.random.rand(1000, 2) for x1, y1 in a: for x2, y2 in b: chebyshev(x1, x2, y1, y2)
Если все выполнено успешно, то вывод будет таким: