Python/C API  -  ускорение Python при помощи кода на C

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

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

Содержание руководства:

  1. Интерпретатор и скорость выполнения кода:
    1.1. Медленный, но гибкий!
    1.2. Почему время выполнения кода так важно?
  2. Маршалинг.
  3. Управление памятью.
  4. Простой пример:
    4.1. Исходный код на языке программирования C.
    4.2. Модуль ctypes: доступ к библиотеке C из Python.
  5. Выводы.

1. Интерпретатор и скорость выполнения

Python  —  это удобный, простой в изучении, кроссплатформенный и легко расширяемый язык программирования с открытым исходным кодом.

Медленный, но гибкий!

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

Python адаптируется к вашему стилю программирования и потребностям, охватывая различные парадигмы, такие как процедурное, функциональное и объектно-ориентированное программирование (ООП).

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

Почему время выполнения кода так важно?

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

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

Как раз такой подход применяется многочисленными Python-библиотеками (например, NumPy, SciPy и т. д.) или фреймворками глубокого обучения (например, TensorFlow, PyTorch и т. д.).

Итак, если вы Data Scientist или инженер по машинному обучению, желающий научиться вызывать функции CUDA, то руководство написано для вас. Начинаем!


2. Маршалинг

Первый шаг, необходимый на пути,  —  понять, что такое маршалинг и как он работает. Из Википедии:

“Маршалинг  —  процесс преобразования информации из оперативной памяти в пригодный для хранения или передачи формат”.

Почему маршалинг важен для темы ускорения Python? Чтобы перенести данные из Python в C или C++, привязки Python должны преобразовать эти данные в пригодную для передачи форму.

В Python абсолютно все  —  это объекты. В зависимости от версии установленного интерпретатора CPython, операционной системы и других нюансов, целое число int занимает различное количество байт в памяти компьютера. С другой стороны, целое число uint8_t в C всегда занимает только 8 бит общей памяти. Таким образом, для начала необходимо как-то согласовать эти два типа данных из двух разных языков программирования.

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


3. Управление памятью

C и Python управляют памятью по-разному. Python автоматически выделяет память для каждого нового объекта, а когда объект больше не нужен, то от него избавляется специальный сборщик мусора, уничтожающий неиспользуемые объекты для освобождения памяти системы.

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

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


4. Простой пример

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

Для выполнения примера вам понадобятся две технологии:

  1. Python 3.6 или более новой версии.
  2. Средства разработки Python, например пакет python3-dev.

Исходный код на языке программирования C

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

#include <stdio.h>


float cadd(int x, float y) {
float res = x + y;
printf("In cadd: int %d float %.1f returning %.1f\n", x, y, res);
return res;
}

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

gcc -shared -Wl,-soname,libcadd -o libcadd.so -fPIC cadd.c

После выполнения команды в вашем рабочем каталоге создастся файл libcadd.so  —  теперь переходите к следующему шагу.

Модуль ctypes: доступ к библиотеке C из Python

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

Чтобы выполнить функцию C cadd из сценария Python, скопируйте приведенный ниже код:

import ctypes
import pathlib

if __name__ == "__main__":
# load the lib
libname = pathlib.Path().absolute() / "libcadd.so"
c_lib = ctypes.CDLL(libname)

x, y = 6, 2.3

# define the return type
c_lib.cadd.restype = ctypes.c_float
# call the function with the correct argument types
res = c_lib.cadd(x, ctypes.c_float(y))
print(f"In Python: int: {x} float {y:.1f} return val {res:.1f}")

В строке 7 написано обращение к библиотеке общего доступа на языке C, созданной нами ранее. В строке 12 объявляется возвращаемый тип данных для функции C cadd. Это очень важно, ведь необходимо, чтобы ctypes всегда знал, как “маршалировать” объекты для корректной передачи в C и какие типы данных ожидать в качестве результата.

То же самое относится и к переменной y в строке 14: необходимо объявить, что она содержит ссылку на тип данных float. Наконец, переменную x можно оставить как есть, потому что по умолчанию ctypes считает, что все переменные  —  это целые числа.

Теперь выполним сценарий с вызовом функции на C точно так же, как и любой другой сценарий в Python-файле:

python3 padd.py

Результат похож на своего рода волшебство:

In cadd: int 6 float 2.3 returning  8.3
In Python: int: 6 float 2.3 return val 8.3

Поздравляем, вы впервые вызвали для выполнения в Python функцию из библиотеки языка C!


5. Выводы

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

Однако у него есть свои недостатки, и самый заметный из них  — скорость выполнения кода интерпретатором CPython. Данная проблема решается интеграцией в Python требовательных функций на другом языке (например, C или C++). Вызываются такие функции при помощи специальных связок Python и C (Python bingings).

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

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Dimitris Poulopoulos: Do You Hate How Slow Python Is? This Is How You Can Make It Run Faster!

Предыдущая статья5 ловких приемов Xcode для рефакторинга кода
Следующая статьяАсинхронное программирование с промисами JavaScript