Всем привет! Недавно работал над своим пет проектом, где нужно было много много считать, а программа не успевала проводить расчеты вовремя. Имею ввиду, что программа работала чуть ли не 15 минут, хотя должна была бы делать это пошустрее, ведь пользователь - человек искушенный.

Итак, предположим, что я пишу программу, по сортировке массивов, банальная история, ничего сложного, давай реализуем ее на Python3 и на языке C в качестве простых функций, на вход которых идут только массивчики(ну в С не совсем массив на вход, ну да ладно, мы сейчас не об этом). Реализация на Python3 будет выглядеть так:

def bubble_sort(arr):
  # метод который реализует свап переменных
    def swap(i, j):
        arr[i], arr[j] = arr[j], arr[i]

    n = len(arr)
    swapped = True
    
    x = -1
    # реализация сортировки
    while swapped:
        swapped = False
        x = x + 1
        for i in range(1, n-x):
            if arr[i - 1] > arr[i]:
                swap(i - 1, i)
                swapped = True

А на языке С так:

#include <stdio.h>

// количество элементов массива
#define len 1000

//инициализация обрабатываемого массива
int a[len];

// Метод по добавлению элементов, которые будем сртировать
void addItemInArray(int id, int val) {
    a[id] = val;
}

// метод сортировки
void sort(void) {
    for(int i = 0 ; i < len - 1; i++) {
        for(int j = 0 ; j < len - i - 1 ; j++) {
            if(a[j] > a[j+1]) {
                int tmp = a[j];
                a[j] = a[j+1] ;
                a[j+1] = tmp;
            }
        }
    }
}

// тестовый метод, для проверки подключения к проекту на Python
void test(int a) {
  printf("%d", a);
}

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

И по той причине что мы будет запускать все из Питона напрямую - припишем простой декоратор на проверку - сколько времени заняло выполнение функции.

# название декоратора, на вход идет функция (автоматически подставляется функция указанная под объявлением декоратора)
def timein(func):
    # функция "обертка", на вход идут аргументы функций, которые будут использовать этот декоратор
    def wrapper(val):
        start = datetime.now()
        # вызов метода, который положили в декоратор
        func(val)
        end = datetime.now()
        print(f"было потрачено времени - {end - start}")
    return wrapper

Что дальше нам нужно - нам нужно скомпилировать C файл для возможности его использования в Python файле, для этого понадобится библиотека ctypes.

# Для Linux
$ gcc -shared -Wl,-soname,sort -o sort.so -fPIC sort.c
​
# Для Mac
$ gcc -shared -Wl,-install_name,sort.so -o sort.so -fPIC sort.c

В результате мы будем использовать файл sort.so и обращаясь к нему - будем вызывать функции. Ну а теперь к самому интересному, подключим библиотеку ctypes и с ее помощью попробуем вызвать тестовый метод.

from ctypes import *

my_lib = CDLL("./sort.so") # Подключаем C-файл

some_val = 100 # значение для тестового метода

# my_lib.test(some_val)  сработает, однако не стоит так делать
# почему не стоит - далее в статье
my_lib.test(c_int(some_val))

Результат работы программы такой:

Отработало корректно(так как тестовый метод должен был просто напечатать переданное в него число типа int), а теперь к тому - почему лучше писать таким образом(c_int(some_val)).
Из-за того, что разные типы данных, имеют разный размер (занимают разное количество ячеек памяти), могут быть проблемы при "смеси" этих значений. Вот табличка с типами:

тип ctypes

тип C

тип Python

c_bool

_Bool

bool (1)

c_char

char

1-символьный байтовый объект

c_wchar

wchar_t

1-символьная строка

c_byte

char

int

c_ubyte

unsigned char

int

c_short

short

int

c_ushort

unsigned short

int

c_int

int

int

c_uint

unsigned int

int

c_long

long

int

c_ulong

unsigned long

int

c_longlong

__int64 or long long

int

c_ulonglong

unsigned __int64 or unsigned long long

int

c_size_t

size_t

int

c_ssize_t

ssize_t or Py_ssize_t

int

c_float

float

float

c_double

double

float

c_longdouble

long double

float

c_char_p

char * (оканчивающийся на NUL)

байтовый объект или None

c_wchar_p

wchar_t * (оканчивающийся на NUL)

string или None

c_void_p

void *

int или None

Теперь подготовим массив значений, который будем сортировать.

for _ in range(1000):
    my_array.append(randint(1, 500))

Теперь подготовим массив в нашем модуле в С.

for i in range(len(my_array)):
    # в качестве первого аргумента передаем номер элемента в массиве, а второе - значение
    my_lib.addItemInArray(i, my_array[i]) 

Ну и последний шаг - запуск сортировки в С и Python.

# реализация метода для вызова функции из С
@timein
def sort_C(a):
    my_lib.sort(a)

sort_C(a) # вызов из С
sort(my_array) # вызов питоновской функции

В итоге код на Python будет выглядеть так:

from datetime import datetime
from ctypes import *
from random import randint

my_lib = CDLL('./sort.so')

def timein(func):
    def wrapper(val):
        start = datetime.now()
        func(val)
        end = datetime.now()
        print(f"было потрачено времени - {end - start}")
    return wrapper

@timein
def sort(array):
    for i in range(len(array) - 1):
        for j in range(len(array) - 2):
            if array[i] > array[j]: 
                array[i], array[j] = array[j], array[i]

my_array = []

for _ in range(1000):
    my_array.append(randint(1, 500))

sort(my_array)

# заполняю массив в С
for i in range(len(my_array)):
    my_lib.addItemInArray(i, my_array[i])

a = 1

@timein
def sort_C(a):
    my_lib.sort(a)

sort_C(a)

И соответственно на С, он будет выглядеть таким образом:

include <stdio.h>

#define len 1000

void test(int a) {
	printf("%d", a);
}

// инициализирую обрабатываемый массив
int a[len];

// Метод по добавлению элементов, которые будем сртировать
void addItemInArray(int id, int val) {
    a[id] = val;
}

// метод сортировки
void sort(int asd) {
    for(int i = 0 ; i < len - 1; i++) {
        for(int j = 0 ; j < len - i - 1 ; j++) {
            if(a[j] > a[j+1]) {
                int tmp = a[j];
                a[j] = a[j+1] ;
                a[j+1] = tmp;
            }
        }
    }
}

И финал - предварительная компиляция и запуск с проверкой - сколько времени потребуется разным языкам справиться с одним и тем же действием:

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

Спасибо за уделенное время.

Комментарии (13)


  1. CrazyElf
    14.06.2023 12:36
    +5

    Именно поэтому математические библиотеки для питона написаны на C++ либо на Fortran и работают довольно быстро, так что обычно самому ничего не нужно писать, просто берёте Numpy и всё считается в питоне моментально. А то, для расчёта чего чего нет готовых методов в Numpy, довольно часто можно ускорить с помощью опять же готовой библиотеки Numba. Ну и есть куча других тонкостей (использование подходящих структур данных и методов, кэширование и т.д.), благодаря чему чистый C++ для питониста почти не нужен.


    1. GedKotlet Автор
      14.06.2023 12:36
      +1

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


    1. VBart
      14.06.2023 12:36
      +1

      Вообще-то Numpy написан с использованием С, а не на С++. Большинство подобных библиотек написано на С, что логично, т.к. сам Python тоже написан на С.


      1. Tujh
        14.06.2023 12:36

        что логично, т.к. сам Python тоже написан на С.

        Одно из другого ни как не следует. А если бы следовало - это означало бы только плохой дизайн API.

        The Application Programmer’s Interface to Python gives C and C++ programmers access to the Python interpreter at a variety of levels. The API is equally usable from C++, but for brevity it is generally referred to as the Python/C API.

        https://docs.python.org/3/c-api/intro.html

        Си проще для небольших библиотек, чем С++ и порог вхождения в Си близок к порогу вхождения в Python, в то время, как для С++ он гораздо выше.


        1. VBart
          14.06.2023 12:36

          "usable" не означает, что это правильно и удобно. Всё-таки писать на С++ в стиле С - довольно неблагодарное занятие, а если не в стиле С, то это означает сильно отличаться от кода стандартных библиотек Python, что также сомнительный выбор для задачи расширения возможностей Python (как и любой необоснованный зоопарк языков в проекте).

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


          1. AMDmi3
            14.06.2023 12:36

            Нет никакого смысла стремиться к тому чтобы код был похож на код стандартных библиотек Python, уж скорее есть смысл в обратном. Удобно и правильно писать на полноценном идиоматичном C++, а сишная прослойка между ним и питоновским API будет очень тонкой и ограниченной конвертацией аргументов и возвращаемых значений между PyObject и плюсовыми типами. В случае же когда код модуля вынужден оперировать PyObject'ами, я вообще считаю преступлением писать руками простыни Py_INC/DEC/XDECREF'ов вместо того чтобы обернуть PyObject в плюсовый класс с поддержкой move semantics и явной передачей владения/заимствованием и писать на нем лаконичный, понятный и безопасный код.


  1. Gadd
    14.06.2023 12:36
    +3

    Никогда не измеряйте время выполнения python кода таким образом. Используйте https://docs.python.org/3/library/timeit.html
    Так же можно использовать соответствующие magic команды в jupyter блокнотах.


    1. GedKotlet Автор
      14.06.2023 12:36
      +2

      Не знал о существовании такой штуки, спасибо за замечание.


  1. HemulGM
    14.06.2023 12:36

    Разве это не было очевидно?


    1. NooneAtAll3
      14.06.2023 12:36

      увы, не все рождаются сразу матёрыми синьорами)


      ну и вообще, в обучении полезно теорию перепроверять на практике


  1. k61n
    14.06.2023 12:36
    +1

    С использованием Python.h можно написать питоний модуль прямо на С, как, собственно, написан сам питон или, например, библиотека парсинга временных меток ciso8601 https://github.com/closeio/ciso8601
    Плюс есть ещё sip, от riverbank computers, тот самый который разрабатывает pyqt, и наверняка ещё пару методов интеграции С/С++ и питона найдётся.

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


  1. Soulskill
    14.06.2023 12:36

    Ещё f2py есть, но это прям для олдов, которым numpy like работа с массивами нужна


  1. spooph
    14.06.2023 12:36

    Ух ты, статья настоящий прорыв! Спасибо автору за тщательно проведенное исследование такой малоизученной темы!