Расширение Python с помощью C

Python

Есть несколько способов ускорения кода Python:

  1. В первую очередь попробуйте сократить временную сложность. Выберите более быстрый алгоритм. В большинстве случаев этого более чем достаточно.
  2. Если первый вариант не работает, попробуйте распараллелить код. При наличии нескольких бездейственных процессов можно разделить задачу на подзадачи, которые будут обрабатываться одновременно. Однако это довольно сложный процесс, который не гарантирует ускорение кода.
  3. Переведите медленные части кода на более быстрый язык. Это довольно простая задача, если вы знаете другой ЯП. Рассмотрим этот вариант.

Поиск простых чисел

Нам нужно вычислить простое число k. Ниже представлен один из способов выполнить это в Python:

def isPrime(n):
  for num in range(2, n//2):
    if n%num == 0:
      return False
  return True

def kth_prime(k):
  candidate = 2
  while k:
    if isPrime(candidate):
      k -= 1
    candidate += 1
  return candidate - 1

# Driver code
print(kth_prime(10000)) # The 10000th prime number is 104723

"""
44.34user 0.00system 0:44.57elapsed 99%CPU (0avgtext+0avgdata 8572maxresident)k
0inputs+0outputs (0major+1587minor)pagefaults 0swaps
"""

Этому коду требуется около 42 секунд для вычисления 10000-го простого числа. Посмотрим, насколько быстро C сможет выполнить эту задачу с тем же алгоритмом:

#include<stdio.h>
#include "primeheader.h"

int isPrime(int n) {
  for (int i=2; i<n/2; i++) {
    if (n%i == 0)
      return 0;
  }
  return 1;
}

int kthPrime(int k) {
  int candidate = 2;
  while (k) {
    if (isPrime(candidate))
      k--;
    candidate++;
  }
  return candidate-1;
}

// Код драйвера
int main() {
  printf("%d\n", kthPrime(10000));
  return 0;
}

extern int kthPrime(int n);

Выполнение заняло всего 1,01 секунды!

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

Для начала создадим простую библиотеку Python с C для вычисления простого числа k. Выполним следующие шаги:

  1. Написание функции на C (этот шаг уже выполнен).
  2. Интеграция функции на C для работы с Python.
  3. Сборка.

Интеграция

Создаем функциональность для реализации CPython — структуры под названием PyObject. На этом этапе нам необходимо преобразовать типы данных C для использования в Python. В данном случае мы конвертируем все элементы в PyObjects.

#include "python3.6m/Python.h" // Python предоставляет API через файл заголовка Python.h
#include "primeheader.h"

// Статическая функция, которая принимает аргументы PyObject и возвращает результат PyObject
static PyObject* py_kthPrime(PyObject* self, PyObject* args) {
  int n;
  if (!PyArg_ParseTuple(args, "i", &n)) // Проверка и анализ аргументов, полученных функцией, на возможность использования в C
    return NULL;
  return Py_BuildValue("i", kthPrime(n)); // Получаем результат из C, упаковываем его вместе с PyObject и возвращаем
}

// Определение коллекции методов, вызываемых из модуля
static PyMethodDef PyFastPrimeMethods[] = {
  {"kthPrime", py_kthPrime, METH_VARARGS, "Finds the kth prime number"}
};

// Определение модуля
static struct PyModuleDef fastprimemodule = {
  PyModuleDef_HEAD_INIT,
  "fastprime",
  "This module calculates the kth prime number",
  -1,
  PyFastPrimeMethods
};

// Этот метод вызывается при импорте кода в Python. Он создает экземпляр модуля и возвращает ссылку на него
PyMODINIT_FUNC PyInit_fastprime(void) 
{ 
  return PyModule_Create(&fastprimemodule); 
} 

Приведенный выше код C может показаться немного сложным тем, кто не знаком с такими сильно типизированными языками, как C++ или Java, но на самом деле он довольно прост.

Как было сказано ранее, все элементы представлены в PyObject. Статическая функция, которая возвращает указатель на PyObject, вызывается CPython при импорте библиотеки и вызове функции kthprime. Эта функция принимает два аргумента: self и args. Первая ссылается на PyObject, который вызвал функцию, или на сам экземпляр модуля.

Python также предоставляет множество методов C для анализа аргументов. PyArg_ParseTuple является одним из них.

Поскольку аргумент k передан как позиционный аргумент, Python передает его в качестве кортежа с одним элементом. «i» сообщает PyArg_ParseTuple о том, что необходимо найти целое число.

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

Сборка

Настало время собрать все части воедино.

Для начала компилируем код c_prime.c в перемещаемый формат. С помощью GCC можно скомпилировать код C в объектный файл, который будет содержать код в файле c_prime.c в формате машинного кода.

Затем компилируем код fastprime.c в объектный код, как было сказано ранее. Теперь соединяем все объектные файлы вместе со стандартной библиотекой для создания финальной версии кода. К счастью, Python предоставляет библиотеку disutils, которая значительно упрощает этот процесс.

from distutils.core import setup, Extension 
  
setup(name='fastprime', 
    ext_modules=[ 
            Extension('fastprime', 
                    ['fastprime.c'],
                    extra_objects=["c_prime.o"] # Relocatable compiled code of the c_prime to prevent recompiling each time
                    ) 
            ] 
)

# python setup.py build

Теперь у нас есть модуль Python с расширением .so (общий объект, аналогичный файлам .dll в Windows). Этот общий объект можно импортировать в Python и использовать для вычисления простого числа k.

import fastprime
print(fastprime.kthPrime(10000))

Этот фрагмент кода также выполняется за 1,01 секунды!

Таким образом, нам удалось ускорить код примерно в 40 раз. Возникает вопрос: почему этот способ так редко используется? На это есть несколько причин.

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

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

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


Перевод статьи Akshay Pai: Extending Python with C