Ускоряем код на Питоне с помощью расширений на Cи

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Производительность Си — в программах на Питоне.

Питон — простой, но мощный язык, который заслуженно стал одним из самых популярных. Тем не менее, иногда ему не хватает скорости статически типизированных языков с предварительной компиляцией, таких как Cи и Джава.

Почему Питон — медленный?

Как известно, код на Питоне обычно выполняется интерпретатором, а это часто очень медленный процесс — если сравнивать с Джавой и Си, в которых исходный код компилируется в машинный или байт-код (к сожалению, тема компиляции выходит за рамки статьи).

Как ускорить код на Питоне?

Обычно производительности Питона достаточно — если не приходится выполнять «тяжелые» вычисления, в случае которых как раз и могут пригодиться расширения на Cи.

Расширения — это возможность написать функцию (на Cи), скомпилировать ее в модуль Питона и использовать в исходном коде как обычную библиотеку.

Многие популярные модули написаны на Си или Cи++ (например, numpy, pandas, tensorflow и т. д.) — для повышения производительности и (или) расширения низкоуровневой функциональности.

О чем важно знать:

  • Расширения на Си работают только на реализации Cpython, но поскольку по умолчанию используется именно она, проблемы в этом быть не должно.

  • Для применения этого подхода рекомендуется иметь базовые знания Си. Но если вы знаете только Питон, статья тоже будет вполне вам понятна.

Как писать расширения на Си

В качестве примера реализуем классическую функцию fib(n), которая принимает число n и возвращает соответствующее число в последовательности Фибоначчи, и сравним производительность версий на Питоне и Си.

Прежде всего нам понадобится API для Питона, Python.hзаголовочный файл Си, который содержит всё необходимое для взаимодействия с Питоном.

Установка API:

  • На Линуксе обычно нужно установить пакет python-dev или python3-dev (если он еще не установлен). (В некоторых дистрибутивах название пакета может отличаться.)

  • В стандартной установке Windows Питон по умолчанию уже есть.

  • В macOS Питон тоже должен быть установлен — если это не так, запустите brew reinstall python.

Теперь откройте любой редактор кода и создайте файл модуля Си. Рекомендуется следовать соглашению об именовании (получится что-то вроде module_name.c), но в целом можете называть как хотите. В статье наш модуль будет называться c_module.c.

Прежде чем писать код расширения, необходимо включить пару основных определений и объявлений:

// This definition is needed for future-proofing your code
// see https://docs.python.org/3/c-api/arg.html#:~:text=Note%20For%20all,always%20define%20PY_SSIZE_T_CLEAN.
#define PY_SSIZE_T_CLEAN
// The actual Python API
#include <Python.h>

Эти строки рекомендуется поместить в начало файла — для обеспечения совместимости.

В Питоне всё является объектом, поэтому наша функция c_fib(n) тоже должна возвращать объект, а именно указатель PyObject (определенный в Python.h).

// pure C function that will be called recursively
int fib(int n)
{
  if (n <= 1)
    return n;
  return fib(n-1) + fib(n-2);
}

// function that will be called from Python code
// wraps around the pure C fib function
PyObject* c_fib(PyObject* self, PyObject* args)
{
  int n;
  PyArg_ParseTuple(args, "i", &n);
  n = fib(n);
  return PyLong_FromLong(n);
}

После этого необходимо объявить, какие функции экспортировать из модуля, чтобы они были доступны из Питона.

// array containing the module's methods' definitions
// put here the methods to export
// the array must end with a {NULL} struct
PyMethodDef module_methods[] = 
{
    {"c_fib", c_fib, METH_VARARGS, "Method description"},
    {NULL} // this struct signals the end of the array
};

// struct representing the module
struct PyModuleDef c_module =
{
    PyModuleDef_HEAD_INIT, // Always initialize this member to PyModuleDef_HEAD_INIT
    "c_module", // module name
    "Module description", // module description
    -1, // module size (more on this later)
    module_methods // methods associated with the module
};

// function that initializes the module
PyMODINIT_FUNC PyInit_c_module()
{
    return PyModule_Create(&c_module);
}

Определение методов модуля

Каждый экспортированный метод представляет собой структуру, содержащую:

  • Имя экспортируемого метода (у нас это «c_fib»).

  • Фактически экспортируемый метод (c_fib).

  • Тип принимаемых методом аргументов (у нас — METH_VARARGS). Из документации по METH_VARARGS: «Это типичное соглашение о вызовах с методами типа PyCFunction. Функция ожидает два значения PyObject*. Первый — это объект self для методов; для функций модуля это объект модуля. Второй параметр (часто его называют args) — объект кортежа, представляющий все аргументы».

  • И const char* с описанием метода.

Определение модуля

Модуль представлен в виде структуры (см. код выше). Код должен быть вполне понятен — вопросы может вызвать разве что аргумент m_size, для которого мы задали значение -1. Выдержка из документации:

Значение -1 для аргумента m_size означает, что модуль не поддерживает субинтерпретаторы, потому что имеет глобальное состояние.

Функция инициализации модуля

При импорте модуля вызывается PyMODINIT_FUNC и инициализирует его. Обратите внимание, что имя функции должно начинаться с PyInit_ и заканчиваться именем модуля — то есть, в нашем примере это будет PyInit_c_module().

Здесь описаны лишь несколько возможностей API для Питона — подробнее можно почитать на странице документации.

Компиляция расширения в модуль

После написания кода на Си нужно скомпилировать его в модуль для Питона — к счастью, для этого есть множество встроенных инструментов.

Создайте скрипт на Питоне (по традиции — setup.py) со следующим кодом:

# import tools to create the C extension
from distutils.core import setup, Extension

module_name = 'c_module'
# the files your extension is comprised of
c_files = ['c_module.c']

extension = Extension(
    module_name,
    c_files
)

setup(
    name=module_name,
    version='1.0',
    description='The package description',
    author='Nicholas Obert',
    author_email='nchlsuba@gmail.com',
    url='https://my.web.site/some_page',
    ext_modules=[extension]
)

Скрипт обладает множеством возможностей, но мы будем использовать только команды build и install. Подробнее смотрите в документации или в выводе с флагом «help»:

python3 setup.py --help

В командной строке выполните следующее:

python3 setup.py build

В результате появится каталог с именем build, внутри которого будут скомпилированные библиотеки. После завершения работы команды выполните:

python3 setup.py install

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

Для этой команды могут понадобиться права администратора или суперпользователя (root). Можно не выполнять установку для всей системы, но в этом случае для использования расширения придется задействовать относительный импорт.

Использование расширения в программе на Питоне

В файле Питона импортируйте только что созданный модуль, используя выбранное имя — в нашем случае это c_module:

import c_module
print(c_module.c_fib(5))
# output: 5

Как видите, расширение используется так же, как и любой другой модуль.

Сравнение с версией на «чистом» Питоне

Теперь сравним функцию c_fib с ее аналогом на Питоне. Воспользуемся встроенным модулем time:

import c_module
from time import time

# Python fib version using recursion
def py_fib(n):
    if (n <= 1):
        return n
    return py_fib(n-1) + py_fib(n-2)

n = 5

# C test
t = time()
c_res = c_module.c_fib(n)
c_time = time() - t

# Python test
t = time()
py_res = py_fib(n)
py_time = time() - t

print(f'Input: {n}\n{py_res=}, {py_time=}\n{c_res=}, {c_time=}')  

Вывод:

Input: 5

py_res=5, py_time=5.245208740234375e-06

c_res=5, c_time=1.6689300537109375e-06

Как и ожидалось, функция на Си работает быстрее.

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

А теперь попробуем на больших числах:

Input: 10

py_res=55, py_time=5.245208740234375e-05

c_res=55, c_time=2.6226043701171875e-06

Input: 30

py_res=832040, py_time=0.40490126609802246

c_res=832040, c_time=0.004115581512451172

Input: 40

py_res=102334155, py_time=50.17047834396362

c_res=102334155, c_time=0.4414968490600586

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

И здесь расширения на Си могут здорово выручить: так вы поручите всю тяжелую работу производительному языку, а в качестве основного продолжите использовать Питон.

Примеры использования

Допустим, есть задача выполнить трудоемкие вычисления — например, для криптографического алгоритма, глубокого машинного обучения или обработки больших объемов данных. В этом случае расширения на Си могут снять нагрузку с интерпретатора Питона и ускорить работу приложения.

Что, если вам нужно создать низкоуровневый интерфейс или работать с памятью непосредственно из Питона? Здесь тоже стоит использовать расширения на Си — если вы знаете, как работать с «чистыми» указателями.

Еще один практический пример — оптимизация уже существующего «подтормаживающего» приложения на Питоне без переписывания его на другом языке.

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

Время — ресурс, которого всегда не хватает. Используйте его с умом.

Заключение

Расширения на Си — отличное дополнение в арсенале разработчика, будь вы фанат производительности и эффективности или любитель смешивать различные технологии и экспериментировать с чем-то новым: вы не только получаете почти «бесплатный» скачок производительности, но и расширяете функциональные возможности Питона, не прибегая к устаревшему стеку технологий.

Благодарю за внимание.


О переводчике

Перевод статьи выполнен в Alconost.

Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики — для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.

Источник: https://habr.com/ru/company/alconost/blog/571366/


Интересные статьи

Интересные статьи

В интернете уже есть множество статей на тему VeraCrypt. Но большинство из них задействуют стандартные настройки, которые уже давно отработаны злоумышленниками и не могут...
Всем привет, меня зовут Дмитрий Кузин (Application Development Senior Analyst в Accenture), и в своей статье я делюсь историей о том, как запрос на решение задачи в корпоративной рассыл...
Всем привет! В предыдущем посте мы рассказали, как применять ACM для сине-зеленого развертывания, миграции приложений между кластерами и аварийного восстановления. Сегодня покажем,...
Flutter предлагает различные виджеты для работы с определенным набором фигур, например, ClipRect, ClipRRect, ClipOval. Но также есть ClipPath, с помощью которого мы можем создавать ...
Руководство по использованию mergeMap и forkJoin вместо простых подписок для множественных запросов к API. В этой статье я покажу два подхода к обработке множественных запросов в Angular с...