Есть несколько способов ускорения кода Python:
Нам нужно вычислить простое число 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. Выполним следующие шаги:
Создаем функциональность для реализации 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
Комментарии